とりゅふの森

GCPデータエンジニアとして生きる

【Python入門】PythonでWeb APIをサクッと作成してみる

こんにちは、今回はPythonのWebフレームワークFlaskを用いて、Web APIを作成する方法についてご紹介します。
Flaskは軽量Webフレームワークで、Pythonで人気のWebフレームワーク、Djangoほど多機能ではありませんが、さくっとAPIを作成できるので、私のような非Webエンジニアでも重宝しています。
今回は、このFlaskを用いて、わずか十数分でWeb APIを作成してみましょう!

今回作るもの

PythonでWebAPIを作成し、HTTPリクエストを受け取ったらデータをJSONで返す簡易的なシステムを作成します。

本記事はWindows 10 Pro + Python 3.10.2で検証していますが、Python3系が動く環境であれば再現可能です。
Pythonの環境がない方は、以下を参考に構築してみてください。

www.true-fly.com

Pythonの必要なライブラリをインストールする

ローカルのPython環境に必要なライブラリをインストールします。

$ pip install flask==2.1.2
$ pip install requests==2.28.1

Webアプリケーションを作成していく

今回は以下のリクエストを受け取るAPIを作成します。

GET /api/hello
GET /api/articles
GET /api/articles/<id>
POST /api/articles

ディレクトリ構成は以下の通りです。

.
├── app
│   ├── apis
│   │   ├── articles.py
│   │   └── hello.py
│   ├── models
│   │   └── articles.py
│   └── server.py
├── request.py
└── run.py

app/server.py

from flask import Flask

from app.apis import hello, articles

app = Flask(__name__)
app.register_blueprint(hello.api)
app.register_blueprint(articles.api)

Flaskアプリケーションを定義するファイルです。
app.register_blueprint()の箇所で、helloarticlesの2つのAPIをFlaskアプリケーションに登録しています。

app/api/hello.py

from flask import Blueprint, jsonify, request

api = Blueprint('hello',
                __name__,
                url_prefix='/api/hello')


@api.route('')
def get():
    name = request.args.get('name', 'World')
    return jsonify({'message': 'Hello ' + name + '!'}), 200

このファイルで/api/helloへのリクエストを受け付けるメソッドを定義します。
api = Blueprint()でAPIを定義(これをapp/server.py で読込している)し、 get()のデコレータ@api.route('')でリクエストを受け取る処理を記述します。
GETのパラメータをは request.args.get('name', 'World')で受け取ります。第1引数がパラメータ名、第2引数がパラメータを指定されなかったときのデフォルト値です。 return jsonify({'message': 'Hello ' + name + '!'}), 200`で、HTTPのレスポンスとなるJSONと、HTTPステータスコードを返します。

app/api/articles.py

from flask import Blueprint, jsonify, request

from app.models import articles

api = Blueprint('article',
                __name__,
                url_prefix='/api/articles')

@api.route('')
def get_all():
    data = articles.get()
    return jsonify(data), 200

@api.route('/<id>')
def get_by_id(id):
    data = articles.get(id)
    if len(data) > 0:
        return jsonify(data), 200
    else:
        return jsonify({'errors': [{'message': 'NOT FOUND'}]}), 404

@api.route('', methods=['POST'])
def post():
    data = articles.post(
        str(request.form['title']),
        str(request.form['link'])
    )
    return jsonify(data), 201

このファイルでは以下のリクエストを処理します。

GET /api/articles
GET /api/articles/<id>
POST /api/articles

GET /api/articlesget_all()でリクエストを受け付けます。 GET /api/articles/<id>get_by_id(id)でリクエストを受け付けます。引数でIDを受け取り、IDに対応するデータを返します。
POST /api/articlesは、post()でリクエストを受け付けます。デコレータを、@api.route('', methods=['POST'])と、対応するHTTPメソッドを定義(デフォルトではGETのみを受け付ける)することで、POSTリクエストを処理することができます。
POSTリクエストで受け取ったデータは、request.form['title']のようにして受け取ることができます。
これらのメソッド内のビジネスロジックは、このファイルではなく、次のファイルに記述しています。

app/api/models/articles.py

def get(id: int = None):
    data = [
        {
            'id': '1',
            'title': 'Cloud FunctionsでGoogle Search Consoleのデータ収集を完全自動化する',
            'link': 'https://www.true-fly.com/entry/2022/04/18/080000',
        },
        {
            'id': '2',
            'title': 'PythonでGoogle Search ConsoleのデータをBigQueryにロードする',
            'link': 'https://www.true-fly.com/entry/2022/04/11/073000',
        },
        {
            'id': '3',
            'title': '【TypeScript超入門】TypeScript + webpackでWebアプリケーション開発環境を構築する',
            'link': 'https://www.true-fly.com/entry/2022/03/14/080000',
        },
    ]
    if id:
        for row in data:
            if row['id'] == id:
                return row
        return {}
    else:
        return data

def post(title:str, link:str):
    print(title)
    print(link)
    return {
        'id': '4',
        'title': title,
        'link': link,
    }

記事のデータを扱うモジュールです。
get()では、idが指定されていれば指定のデータを、存在しなければ空の辞書を、指定されていなければ全データを返します。
post()では実際にDBにデータを登録する処理は記述せず、値を受け取ったら標準出力して、辞書の形でデータを返すようにしています。

run.py

import logging
import os

logging.basicConfig(level=logging.DEBUG)

from app import server
if __name__ == '__main__':
    server.app.run(host='localhost', port=int(os.environ.get('PORT', 9000)))

ローカルホストのPORT9000で開発用HTTPサーバを起動するモジュールです。

以上でWebAPIの完成です!
次に、このWeb APIにリクエストを送るモジュールを作成してみます。

request.py

import requests

def get(url):
    response = requests.get(url)
    print(url)
    print(response.status_code)
    print(response.text)

def post(url, data):
    response = requests.post(url, data=data)
    print(url)
    print(response.status_code)
    print(response.text)

if __name__ == '__main__':
    # helloパラメータなし
    url = 'http://localhost:9000/api/hello'
    get(url)
    # helloパラメータあり
    url = 'http://localhost:9000/api/hello?name=Truefly'
    get(url)

    # articles 全件取得
    url = 'http://localhost:9000/api/articles'
    get(url)
    # articles ID1取得
    url = 'http://localhost:9000/api/articles/1'
    get(url)
    # articles ID7取得
    url = 'http://localhost:9000/api/articles/7'
    get(url)

    # ariticles 登録    
    url = 'http://localhost:9000/api/articles'
    data = {
        'title': 'TypeScriptはじめました',
        'link': 'https://www.true-fly.com/entry/2022/03/09/073000',
    }
    post(url, data)

Flaskアプリケーションを起動した状態で、このモジュールを実行すればAPIのテスト実行できます。

実際に実行してみる

全ファイルを作成したら、ターミナルでrun.pyを実行してWebサーバを起動させます。

$ python run.py 
 * Serving Flask app 'app.server' (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: off
INFO:werkzeug: * Running on http://localhost:9000 (Press CTRL+C to quit)

次に別のターミナルを起動し、request.pyを実行して、APIリクエストを送ってみましょう。

$ python request.py
http://localhost:9000/api/hello
200
{"message":"Hello World!"}

http://localhost:9000/api/hello?name=Truefly
200
{"message":"Hello Truefly!"}

http://localhost:9000/api/articles
200
[{"id":"1","link":"https://www.true-fly.com/entry/2022/04/18/080000","title":"Cloud Functions\u3067Google Search Console\u306e\u30c7\u30fc\u30bf\u53ce\u96c6\u3092\u5b8c\u5168\u81ea\u52d5\u5316\u3059\u308b"},{"id":"2","link":"https://www.true-fly.com/entry/2022/04/11/073000","title":"Python\u3067Google Search Console\u306e\u30c7\u30fc\u30bf\u3092BigQuery\u306b\u30ed\u30fc\u30c9\u3059\u308b"},{"id":"3","link":"https://www.true-fly.com/entry/2022/03/14/080000","title":"\u3010TypeScript\u8d85\u5165\u9580\u3011TypeScript + webpack\u3067Web\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u958b\u767a\u74b0\u5883\u3092\u69cb\u7bc9\u3059\u308b"}]

http://localhost:9000/api/articles/1
200
{"id":"1","link":"https://www.true-fly.com/entry/2022/04/18/080000","title":"Cloud Functions\u3067Google Search Console\u306e\u30c7\u30fc\u30bf\u53ce\u96c6\u3092\u5b8c\u5168\u81ea\u52d5\u5316\u3059\u308b"}

http://localhost:9000/api/articles/7
404
{"errors":[{"message":"NOT FOUND"}]}

http://localhost:9000/api/articles
201
{"id":"4","link":"https://www.true-fly.com/entry/2022/03/09/073000","title":"TypeScript\u306f\u3058\u3081\u307e\u3057\u305f"}

まとめ

以上、今日はPythonで簡単なWeb APIを作る方法について紹介しました。
今回はローカル環境での実装まででしたが、GCPのサービスを Flaskについては当ブログでも何度か取り上げてますので、参考になれば幸いです。