はじめに
仕事柄、Watson Machine Learning / Watson APIを呼び出すアプリをIBM Cloud上でよく作っています。
対象がWatson APIの時は Watson APIを使ったNode.jsアプリの標準実装パターンのとおり、サーバーサイドをNode.jsで実装することがほとんどだったのですが、相手がWatson Machine Learningになると、トークン取得、予測API呼出しの二段階のAPI呼出しが必要になり、非同期通信しか使えないNode.jsでの実装が面倒になってきました。
そんなこともあり、最近はひたすらPython + Flaskでサーバーサイドを実装するということをしています。
そこで、PythonをIBM Cloudで動かすためのお作法、クライアントサイドのJavaScriptからサーバーサイドのFlaskにパラメータを渡すときのお作法をメモとしてまとめました。
サンプルコード
サンプルコードはいつものようにGithubにあげてあります。
アプリの画面イメージはこちら。
ブラウザから引数1、引数2を読み込んで、/sendメソッドでサーバーサイドのFlaskに送り、サーバー側で足し算した結果を返すというシンプルなものです。
IBM Cloudへの導入手順はリンク先のGithub README.mdに記載していますが、
$ cf login
$ cf push <service name>
とするだけで可能です。
IBM Cloudにデプロイするためのお作法
Githubにアップしているソースのトップレベルのリストを以下に示します。
-rw-r--r--@ 1 makaishi staff 5 2 8 20:01 .cfignore
drwxr-xr-x 13 makaishi staff 416 6 2 15:47 .git
-rw-r--r--@ 1 makaishi staff 15 4 30 09:26 .gitignore
-rw-r--r-- 1 makaishi staff 6 6 2 15:03 .python-version
-rw-r--r--@ 1 makaishi staff 11357 2 8 09:06 LICENSE
-rw-r--r--@ 1 makaishi staff 22 2 8 09:06 Procfile
-rw-r--r--@ 1 makaishi staff 3674 6 2 15:31 README.md
-rw-r--r--@ 1 makaishi staff 43 6 2 14:43 manifest.yml
drwxr-xr-x 5 makaishi staff 160 6 2 15:30 readme_images
-rw-r--r--@ 1 makaishi staff 15 6 2 14:42 requirements.txt
-rw-r--r--@ 1 makaishi staff 970 6 2 15:42 server.py
drwxr-xr-x 5 makaishi staff 160 5 29 14:08 static
drwxr-xr-x 5 makaishi staff 160 6 2 14:49 templates
この中で重要なものについてそれぞれ説明します。
manifest.yml
Cloud Foundryにデプロイする際に必要な情報を定義するファイルです。
今回のサンプルでは利用していませんが,IBM Cloudの他のサービスをバインドする際には、このファイルにバインド先定義名を記載します。
---
applications:
- path: .
memory: 128M
例えば、Pythonの中でWatson Machine Learningを呼び出している場合は、次のような形になります。
---
declared-services:
machine-learning-1:
label: pm-20
plan: lite
applications:
- path: .
memory: 128M
services:
- machine-learning-1
requirements.txt
Cloud Foundry標準のPython環境で不足するモジュール名を列挙して、デプロイ時に追加導入するモジュールを指定します。今回のサンプルでは以下の2行があるのみです。
requests
flask
例えば、Watson Machine Learningを呼び出すアプリの場合、こんな設定になっています。
cfenv
requests
flask
Image
python-dotenv
numpy
Procfile
WEBサービス起動時のコマンドを指定します。今回の例では以下の通りです。
web: python server.py
server.py
サーバーサービスのPythonプログラムです。今回の例では以下のようになっています。
#!/usr/bin/python
# -*- coding: utf-8 -*-
import urllib3, requests, json
import os
from os.path import join, dirname
from flask import Flask, request, render_template
app = Flask(__name__)
@app.route('/')
def top():
name = "Top"
return render_template('sample.html', title='Flask Sample Web', name=name)
# 「送信」ボタンが押された時の処理
@app.route('/send', methods=['POST'])
def send():
# パラメータの受け取り
req_json = request.json
arg1 = req_json['ARG1']
arg2 = req_json['ARG2']
# ダミーのサーバー処理
sum = arg1 + arg2
print(arg1, arg2, sum)
# 戻り用変数の定義
ret = {'ARG1': arg1, 'ARG2': arg2, 'SUM': sum}
# json.dumps の結果を戻り値とする
return(json.dumps(ret))
@app.route('/favicon.ico')
def favicon():
return ""
port = os.getenv('VCAP_APP_PORT', '8000')
if __name__ == '__main__':
app.run(host='0.0.0.0', port=int(port), debug=True)
この中で重要なのは、クライアントからのパラメータ受け取りの実装と、戻り値の受け渡し方法です。
それぞれ、次のようになっています。どちらもJSONで行うところがポイントです。
受け取り
# パラメータの受け取り
req_json = request.json
arg1 = req_json['ARG1']
arg2 = req_json['ARG2']
戻し
# 戻り用変数の定義
ret = {'ARG1': arg1, 'ARG2': arg2, 'SUM': sum}
# json.dumps の結果を戻り値とする
return(json.dumps(ret))
templates/sample.html
FlaskをWebサーバーとして動かす際のHTMLファイルです。
server.pyの中のrender_templare関数呼び出しで、ファイル名を指定します。
具体的なソースは次のとおりです。
<html>
<head>
{% extends "layout.html" %}
{% block body %}
</head>
<body>
<h2>Flask サンプルアプリ</h2>
<hr>
<div>
<label for="arg1_id">引数1:</label>
<input type="text" value="123.45" name="arg1_id" id="arg1_id" />
<br>
<label for="arg1_id">引数2:</label>
<input type="text" value="0.678" name="arg2_id" id="arg2_id" />
<br>
<label for="sum_id">合計: </label>
<input type="text" name="sum_id" disabled="disabled" id="sum_id" />
<br>
<br>
<input type="button" name="button" value="送信" id="send"/>
<hr>
</body>
</html>
<script>
$(function(){
$('#send').click(send)
});
function call_flask( type, url, error ) {
// 画面からパラメータを取得して辞書データにする
data1 = {
"ARG1": parseFloat($('#arg1_id').val()),
"ARG2": parseFloat($('#arg2_id').val())
};
console.log(data1);
// JSON.stringifyで辞書データをJSON形式にシリアライズする
json1 = JSON.stringify(data1);
// ajax関数でサーバーサイド機能の呼出し
$.ajax({
type: type,
url: url,
dataType: "json",
data: json1,
processData: false,
contentType: "application/json",
cache: false,
timeout: 600000,
success: function (data2) {
console.log('send callback')
console.log(data2);
sum = data2['SUM']
console.log(sum)
$('#sum_id').val(sum)
}
});
}
function send() {
call_flask( 'POST', '/send',
function(XMLHttpRequest,textStatus,errorThrown){alert('error');} );
}
</script>
{% endblock %}
このHTMLで重要なのは、画面項目から値を取ってきて、ajaxで引数を渡すところです。
その実装は以下のとおりとなっています。
画面から値を取ってきて、Javascriptオブジェクトとして組み立てた後、JSON.stringify関数でデータをJSON形式にシリアライズしてからajax関数の引数dataで渡すところがポイントになります。
// 画面からパラメータを取得して辞書データにする
data1 = {
"ARG1": parseFloat($('#arg1_id').val()),
"ARG2": parseFloat($('#arg2_id').val())
};
// JSON.stringifyで辞書データをJSON形式にシリアライズする
json1 = JSON.stringify(data1);
// ajax関数でサーバーサイド機能の呼出し
$.ajax({
type: type,
url: url,
dataType: "json",
data: json1,
processData: false,
contentType: "application/json",
cache: false,
timeout: 600000,
success: function (data2) { // 以下略
templates/layout.html
sample.htmlから呼び出される雛形HTMLです。
2つのHTMLを合成するレンダリング処理はFlaskが行います。
<!doctype html>
<title>Python Flask サンプル</title>
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}">
<link href='https://fonts.googleapis.com/css?family=Lato:300,400,300' rel='stylesheet' type='text/css'>
<script src="{{ url_for('static', filename='jquery.js') }}"></script>
<div class=page>
{% block body %}{% endblock %}
</div>
LICENSE
ソースをWebで公開する際のライセンスの記述を行います。
私はApacheライセンスを使っています。