FlaskによるWebサービスの実装 Part1
ここでは、プラットフォームでAnaconda Pythonを操作するため基本は理解しているものと想定しています。以後に示すコマンドラインの例は、Unixライクなオペレーティングシステムでの例です。
インストール
既にCONDA環境になっているのであれば抜けておきましょう。
code: bash
$ conda deactivate
conda環境を作ります。このとき python と curl をインストールしておきます。
Webブラウザーを使用してすべてのタイプのHTTPリクエストを簡単に生成することはできないため、代わりに curl を使用します。
code: bash
$ conda create -y -n flask_todo python=3.6 curl
$ conda activate flask_todo
Flask と関連パッケージをインストールしておきましょう。
code: bash
$ pip install flask flask-WTF flask-migrate flask-sqlalchemy python-dotenv
Flaskの動作確認
アプリケーションのディレクトリを作成します。
code: bash
$ mkdir -p $HOME/flask/todo_apiv1
$ cd $HOME/falsk/todo_apiv1
app.py を作成します。
code: app.py
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return "Hello, World!"
if __name__ == '__main__':
app.run(debug=True, port=8080)
この段階でFlaskが正常に動作するか確認しましょう。
code: bash
$ python app.py
ブラウザでhttp://127.0.0.1:8080/ にアクセスして "Hello, World!" と表示されれば、Flask は正常に動作していることになります。
RESTful Webサービスの実装
FlaskでWebサービスを構築することは、サーバー側アプリケーションを構築するよりもはるかに簡単です。
RESTful Webサービスの構築に便利なFlask拡張機能はいくつかありますが、今回のTODOサービスは非常に単純なので、拡張機能を使用しなくても実装することができます。
Webサービスのクライアントは、TODOサービスにタスクの追加、削除、変更をリクエストすることになるので、タスクを保存する方法が必要になります。このために、通常はデータベースを構築するわけですが、今回はRESTful Webサービスに焦点を当てるために、もっと単純にタスクリストをメモリに保存するようにします。
この実装方法では、アプリケーションを実行するWebサーバーがシングルプロセスでシングルスレッドの場合にのみ機能します。これは、Flaskの開発用Webサーバーでは問題ありません。データベース設定していないのでタスクデータの永続的な保存ができません。本番Webサーバーでこの手法を使用することはできません。
Webサービスの最初のエントリポイントを実装してみましょう。
まず、単純に辞書型データのリストとしてタスクリストを作成します。
code: tasks.py
tasks = [
{
'id': 1,
'title': 'Buy Beer',
'description': 'IPA 6 bottles',
'done': False
},
{
'id': 2,
'title': 'Buy groceries',
'description': 'Beef, Tofu, Sting Onion',
'done': False
}
]
GETメソッドでタスクリストを取得
app.py で URIとその動作を定義します。
code: app.py
from flask import Flask, jsonify
from tasks import tasks
app = Flask(__name__)
@app.route('/todo/api/v1.0/tasks', methods='GET') def get_tasklist():
return jsonify({'tasks': tasks})
if __name__ == '__main__':
app.run(debug=True, port=8080)
関数get_tasklist() の応答はテキストではなく、Flaskのjsonify()関数でデータ構造から生成したJSONデータで応答しています。
code: bash
$ python app.py
別のターミナルから次を実行して動作確認します。
code: bash
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 268
Server: Werkzeug/1.0.1 Python/3.6.10
Date: Wed, 13 May 2020 13:51:43 GMT
{
"tasks": [
{
"description": "IPA 6 bottles",
"done": false,
"id": 1,
"title": "Buy Beer"
},
{
"description": "Beef, Tofu, Sting Onion",
"done": false,
"id": 2,
"title": "Buy groceries"
}
]
}
RESTfulサービスで関数を呼び出せたことを確認できました。
GETメソッドで指定したタスクを取得
次に、特定のタスクを取得できるように修正しましょう。
code: app.py
from flask import Flask, jsonify, abort
from tasks import tasks
app = Flask(__name__)
@app.route('/todo/api/v1.0/tasks/<int:task_id>', methods='GET') def get_task(task_id):
task = [task for task in tasks if task'id' == task_id] if len(task) == 0:
abort(404)
return jsonify({'task': task0}) @app.route('/todo/api/v1.0/tasks', methods='GET') def get_tasklist():
return jsonify({'tasks': tasks})
if __name__ == '__main__':
app.run(debug=True, port=8080)
関数get_task()は、URLで与えたタスクのIDを取得し、Flaskはそれを関数の引数 task_idに変換します。
この引数を使用して、リストtasksを検索します。 指定したIDがデータベースに存在しない場合は、HTTPエラーコード404を返すように abort() を呼び出しています。
該当するタスクが存在していれば、関数jsonify()を使用してJSONフォーマットにした応答を送信しています。
curl で呼び出してみましょう。
code: bash
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 115
Server: Werkzeug/1.0.1 Python/3.6.10
Date: Wed, 13 May 2020 14:02:51 GMT
{
"task": {
"description": "IPA 6 bottles",
"done": false,
"id": 1,
"title": "Buy Beer"
}
}
うまく関数get_task() を呼び出せています。
存在しないIDを与えてみましょう。
code: bash
HTTP/1.0 404 NOT FOUND
Content-Type: text/html; charset=utf-8
Content-Length: 232
Server: Werkzeug/1.0.1 Python/3.6.10
Date: Thu, 13 May 2020 14:02:59 GMT
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>
存在しないIDを与えると404エラーが返されます。これは、Flaskでabort(404)が実行されるため、デフォルトのエラー404応答を生成しているからです。
REST API はデータ形式については規定していませんが、統一したインタフェースにすることが制約になっています。このため、タスクをJSONで返したのであれば、エラーについてもJSONで応答する必要があります。
そこで、エラーハンドラーを追加しましょう。
code: app.py
from flask import Flask, jsonify, abort, make_response
from tasks import tasks
app = Flask(__name__)
@app.errorhandler(404)
def not_found(error):
return make_response(jsonify({'error': 'Not found'}), 404)
@app.route('/todo/api/v1.0/tasks/<int:task_id>', methods='GET') def get_task(task_id):
task = [task for task in tasks if task'id' == task_id] if len(task) == 0:
abort(404)
return jsonify({'task': task0}) @app.route('/todo/api/v1.0/tasks', methods='GET') def get_tasklist():
return jsonify({'tasks': tasks})
if __name__ == '__main__':
app.run(debug=True, port=8080)
curl を実行して動作確認してみます。
code: bash
HTTP/1.0 404 NOT FOUND
Content-Type: application/json
Content-Length: 27
Server: Werkzeug/1.0.1 Python/3.6.10
Date: Wed, 13 May 2020 14:21:43 GMT
{
"error": "Not found"
}
POSTメソッドでタスクを追加
今度はPOSTメソッドでタスクを追加できるようにしましょう。
app.py に関数create_task()を追加します。
code:python
from flask import Flask, jsonify, abort, make_response, request
from tasks import tasks
# ...
@app.route('/todo/api/v1.0/tasks', methods='POST') def create_task():
if not request.json or not 'title' in request.json:
abort(400)
task = {
'description': request.json.get('description', ""),
'done': False
}
tasks.append(task)
return jsonify({'task': task}), 201
POSTメソッドで呼び出される関数create_task()は、request.jsonを読み取ります。
このrequest.jsonにはリクエストデータがJSON形式で含まれていますが、ContentTypeがJSONとしてリクエストされたときだけです。データが存在しないか、titleがないときは、HTTPエラーコード400(不正なリクエスト)を返します。
次に、最後のタスクのIDに1を加えた新しいタスクを辞書型で追加します。
descriptionフィールドにデータがなくてもよく、doneフィールドは常にFalseに設定するようにしています。
新しいタスクをタスク配列に追加し、追加されたタスクでクライアントに応答し、HTTPステータスコード201(”CREATED”のコード)を返します。
この新しい関数をテストするには、次のcurlコマンドを使用できます。
code: bash
$ curl -i -H "Content-Type: application/json" -X POST \
HTTP/1.0 201 CREATED
Content-Type: application/json
Content-Length: 101
Server: Werkzeug/1.0.1 Python/3.6.10
Date: Wed, 13 May 2020 14:41:36 GMT
{
"task": {
"description": "",
"done": false,
"id": 3,
"title": "Cooking"
}
}
code: bash
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 101
Server: Werkzeug/1.0.1 Python/3.6.10
Date: Wed, 13 May 2020 14:48:36 GMT
{
"task": {
"description": "",
"done": false,
"id": 3,
"title": "Cooking"
}
}
Linuxや bash on WIndows、Cygwin などではこれは正常に動作します。
Windowsの commandターミナルではリクエストの本文を囲むために二重引用符を使用する必要があります。JSONはキワードをのダブルクォートで囲む必要があり、これを正しく伝えるためには次のようにします。
code: windows
PUTメソッドでタスクを更新
PUTメソッドでタスクを更新できるようにしましょう。
app.py に関数update_task()を追加します。
code: python
@app.route('/todo/api/v1.0/tasks/<int:task_id>', methods='PUT') def update_task(task_id):
task = [task for task in tasks if task'id' == task_id] if len(task) == 0:
abort(404)
if not request.json:
abort(400)
if ('title' in request.json and
isinstance(type(request.json'title'), unicode)): abort(400)
if ('description' in request.json and
abort(400)
if ('done' in request.json and
type(request.json'done') is not bool): abort(400)
return jsonify({'task': task0}) 先程追加したIDが3となるタスクは、app.py が更新されたことでメモリから消えているため、追加してから修正してみましょう。
code: bash
HTTP/1.0 201 CREATED
Content-Type: application/json
Content-Length: 101
Server: Werkzeug/1.0.1 Python/3.6.10
Date: Wed, 13 May 2020 15:37:51 GMT
{
"task": {
"description": "",
"done": false,
"id": 3,
"title": "Cooking"
}
}
code: bash
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 101
Server: Werkzeug/1.0.1 Python/3.6.10
Date: Wed, 13 May 2020 15:37:53 GMT
{
"task": {
"description": "",
"done": false,
"id": 3,
"title": "Cooking"
}
}
code: bash
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 104
Server: Werkzeug/1.0.1 Python/3.6.10
Date: Wed, 13 May 2020 15:37:59 GMT
{
"task": {
"description": "",
"done": false,
"id": 3,
"title": "Gymnastics"
}
}
code: bash
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 104
Server: Werkzeug/1.0.1 Python/3.6.10
Date: Wed, 13 May 2020 15:38:21 GMT
{
"task": {
"description": "",
"done": false,
"id": 3,
"title": "Gymnastics"
}
}
DELETEメソッドで指定したタスクを削除
DELETEメソッドでタスクを削除できるようにしましょう。
app.py に関数delete_task()を追加します。
code: python
@app.route('/todo/api/v1.0/tasks/<int:task_id>', methods='DELETE') def delete_task(task_id):
task = [task for task in tasks if task'id' == task_id] if len(task) == 0:
abort(404)
return jsonify({'result': True})
はじめにタスクを追加しておきます。
code: bash
HTTP/1.0 201 CREATED
Content-Type: application/json
Content-Length: 101
Server: Werkzeug/1.0.1 Python/3.6.10
Date: Wed, 13 May 2020 15:52:35 GMT
{
"task": {
"description": "",
"done": false,
"id": 3,
"title": "Cooking"
}
}
code: bash
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 101
Server: Werkzeug/1.0.1 Python/3.6.10
Date: Wed, 13 May 2020 15:52:40 GMT
{
"task": {
"description": "",
"done": false,
"id": 3,
"title": "Cooking"
}
}
このタスクを削除します。
code: bash
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 21
Server: Werkzeug/1.0.1 Python/3.6.10
Date: Wed, 13 May 2020 15:52:47 GMT
{
"result": true
}
もう一度GETして確認しましょう。
code: bash
HTTP/1.0 404 NOT FOUND
Content-Type: application/json
Content-Length: 27
Server: Werkzeug/1.0.1 Python/3.6.10
Date: Wed, 13 May 2020 15:52:50 GMT
{
"error": "Not found"
}
ここまでのまとめ
ここまでで、app.py は次のようになります。
code: app.py
rom flask import Flask, jsonify, abort, make_response, request
from tasks import tasks
app = Flask(__name__)
@app.errorhandler(400)
def bad_request(error):
return make_response(jsonify({'error': 'Bad request'}), 400)
@app.errorhandler(404)
def not_found(error):
return make_response(jsonify({'error': 'Not found'}), 404)
@app.route('/todo/api/v1.0/tasks/<int:task_id>', methods='GET') def get_task(task_id):
task = [task for task in tasks if task'id' == task_id] if len(task) == 0:
abort(404)
return jsonify({'task': task0}) @app.route('/todo/api/v1.0/tasks', methods='GET') def get_tasklist():
return jsonify({'tasks': tasks})
@app.route('/todo/api/v1.0/tasks', methods='POST') def create_task():
if not request.json or not 'title' in request.json:
abort(400)
task = {
'description': request.json.get('description', ""),
'done': False
}
tasks.append(task)
return jsonify({'task': task}), 201
@app.route('/todo/api/v1.0/tasks/<int:task_id>', methods='PUT') def update_task(task_id):
task = [task for task in tasks if task'id' == task_id] if len(task) == 0:
abort(404)
if not request.json:
abort(400)
if ('title' in request.json and
isinstance(type(request.json'title'), unicode)): abort(400)
if ('description' in request.json and
abort(400)
if ('done' in request.json and
type(request.json'done') is not bool): abort(400)
return jsonify({'task': task0}) @app.route('/todo/api/v1.0/tasks/<int:task_id>', methods='DELETE') def delete_task(task_id):
task = [task for task in tasks if task'id' == task_id] if len(task) == 0:
abort(404)
return jsonify({'result': True})
if __name__ == '__main__':
app.run(debug=True, port=8080)
Flaskを使ってRESTful Webサービスを実装することが簡単にできることが確認できました。