前記事までに書ききれなかった話題をちょっと続けます。
サーバの常駐と停止
bottleを使っていて実際に常駐させておこうとすると、起動・停止方法が悩みどころです。
bottle.run()
を呼ぶと、基本的にはコンソールでCtrl+C
を入力してプロセスを中止するまでそのままです。また、SSHのセッションを切ると停止するので、繋ぎっぱなしにしなければいけないの?ってなります。
サーバのバックグラウンド実行
Bottledaemon
などモジュールを別途インストールしたり、サービスとして設定することで、デーモン化する方法もあるようですが、「一つのファイルで動く」というbottle.py
本来の良さをどんどん失っているような気も。
まあ単にSSHを切った後もバックグラウンドで動かすだけならnohup
で充分でしょう。
1 | $ nohup python app.py >> ./server.log & |
上のコマンドはnohup
でバックグラウンド実行しつつ、標準出力(stdout)をログファイル(server.log
という名前)にリダイレクトするものです。これでSSHのセッションを閉じてもサーバが動き続けます。
ps u
を使えば、バックグラウンドで立ち上がっているプロセスやそのPIDが確認できます。下の例ではPID 7609がBottleを実行しているプロセスですね。
1 2 3 4 5 | $ ps u USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND user 1546 0.0 0.1 23720 5288 pts/0 Ss 02:31 0:00 -bash user 7609 1.0 0.4 346188 16228 pts/0 Sl 02:39 0:00 python app.py user 7617 0.0 0.0 38376 3208 pts/0 R+ 02:39 0:00 ps u |
サーバの停止
nohup
でバックグラウンド実行させた後、不要になれば停止したいのですが、これもよくあるPythonスクリプトとは違った癖があります。
スクリプトを止める常套手段、例えばsys.exit()
を使っても、bottle側で例外を握りつぶすので停止しません。WGSIサーバにはサーバをシャットダウンするインタフェース(メソッド)が用意されているようですが、ネットで調べるとbottle.py
のクラスを継承して、、、とか面倒なことが書いてあります。ちょっと困りものです。
一番シンプルなのはおそらく、ふだんnohup
で起動したプロセスを止める時のように、単純にプロセスをkill
するやり方でしょう。
以下は、ブラウザからサーバを停止できるように、route.py
にルーティングとして追加する例です。この例では/stop
ページを踏むと自プロセスを中止する実装になっています。
1 2 3 4 5 | @route('/stop', method=['GET']) def stop_server(): import os import signal os.kill(os.getpid(), signal.SIGTERM) |
クライアントの作成
Webページと違ってAPIのテストとなると、リクエスト(今回はJSON形式)を整形するなど、単にブラウザで表示をチェックしてOK、というわけにはいきませんね。
ChromeやFirefoxといったブラウザにはREST APIをテストするための拡張機能がありますが、パターンを増やそうとすると手動ではツラいものがあります。サーバの実装に合わせて、Pythonで作ってしまいましょう。
requestsモジュールのインストール
Pythonでhttpクライアントと言えばurllib
, urllib2
など色々なライブラリがありますが、増改築を重ねた経歴からやや使いにくい印象。
別途インストールが必要なものの、requests
モジュールを使えば記述がシンプルになります。いつものごとくpipを使って簡単にインストールできます。
1 | $ pip install requests |
requestsモジュールの使用例(GET)
前記事までに作成したサーバはJSONを解釈する仕様ですので、ここでのAPIリクエストはJSONデータをボディとして送信します。
まずはrequests.get()
メソッドを使ったGetリクエストの例です。
対象のURLに対して単純にgetメソッドを使用すると、レスポンスが返ってくるまで同期的に待機し、返り値を調べることでレスポンスを取得できます。
status_code
プロパティにHTTPレスポンスコードがセットされているので、これを調べることでAPIの処理が成功したか否かを調べます。通常はrequests.code.ok
(実体は200
、整数型(int))が期待通りの動作で、それ以外のステータスコードはエラーやタイムアウト(サーバと通信ができなかった場合)を表します。
requests
経由で受信したJSONデータは、.json()
メソッドを使えばそのままオブジェクトとして取り出すことができます。何らかの理由でJSONデータがセットされていない場合は例外が発生します。
これらの動作は基本的に他のメソッド(PUT, DELETE..)でも同じです。
1 2 3 4 5 6 7 8 9 10 11 12 | import requests # URIの作成 api_get_uri = "http://{}:{}/api/item".format(remote_host, remote_port) # リクエストの送信 res = requests.get(api_get_uri) # ステータスコードのチェック if res.status_code != requests.codes.ok: raise Exception("response status not expected") # レスポンス(JSONデータ)の解釈 data = res.json() |
requestsモジュールの使用例(POST)
Postリクエストの例も記載します。
クエリパラメータは辞書型(key-value)で作成してparams
オプションに引数として渡せば、内部でURL(http://… /api?(key)=(value)の形式)としてエンコードされます。
また、リクエストにセットするJSONデータ(リスト、辞書またはそれらがネストされたオブジェクト)は、あらかじめJSON文字列に変換しておきます(json.dumps(object)
メソッド)。合わせてJSONデータであることを明示するためのヘッダ('application/json'
)をセットしますが、やり方はクエリパラメータと同じく辞書型を渡すだけです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | import requests import json # リクエストを作成するテスト用のモジュール import random from datetime import datetime as dt # URIの作成 api_post_uri = "http://{}:{}/api/item".format(remote_host, remote_port) # クエリパラメータとヘッダの作成 param = {"itemtype": "file"} headers = {'content-type': 'application/json'} # ボディ(JSON形式の文字列)の作成 payload = json.dumps({"data": "test data from client", "timestamp": dt.now().isoformat(), "val": random.random()}) # リクエストの送信 res = requests.post(api_post_uri, params=param, data=payload, headers=headers) # ステータスコードのチェック if res.status_code != requests.codes.ok: raise Exception("response status not expected") # レスポンス(JSONデータ)の解釈 data = res.json() |