読者です 読者をやめる 読者になる 読者になる

Jの衝動書き日記

さらりーまんSEの日記でございます。

Pythonで簡単にWebサービス構築-JSON編

技術

 前回、PythonWebサービス構築用フレームワークのtornadoについて紹介したが、今回はJSONに特化した拡張ライブラリ-Tornado-JSONについて紹介する。

 

Tornado-JSONのインストール

 tornadoと同様にpipを使おう。

    pip install Tornado-JSON

特徴

  • Webapiの入出力形式にJSONを採用しそれに特化することで使いやすくしている
  • パッケージ名・モジュール名で自動的にURLを決定
  • jsonschemaによるバリデーション実行をより使いやすくする
  • APIドキュメントの自動生成が可能

 

Webサービス構築手順

使用するクラス

  • from tornado.ioloop import IOLoop 
  • from tornado_json.routes import get_routes
  • from tornado_json.application import Application
  • from tornado_json.requesthandlers import APIHandler
  • from tornado_json import schema

  ※Applicationはクラス名がtornadoと一緒だがパッケージが異なる

手順

1. HTTP REquestに応答するためのHandlerを作成する。APIHandlerを継承したクラスを作成し、getやpostメソッドを実装する。

class HelloWorldHandler(APIHandler):
   
      @schema.validate(
          output_schema={"type":"string"},
      )
      def get(self):
          return "Hello world!"

 ※応答結果はreturnで返す
   ※@schema.validateで応答結果の形式を指定する必要がある

 

2. 1を入れるパッケージを作成する。ディレクトリを作成し、その中に1で作成したクラスのファイルと__init__.pyを入れる。

$ ls helloworld
__init__.py api.py

 

3. Handler探索を行う。パッケージを指定することで、その配下にあるAPIHandlerを継承したクラスを探索し自動的にURLを作成する。

   import helloworld
   routes = get_routes(helloworld)

 

4. Applicationを作成する。3.で作成したroutesを引数に指定して作成する

   settings = {"debug":True }
   Application(routes=routes, settings=settings, generate_docs=True)

  ※tornado.web.Applicationとは引数が異なるので注意!

5.  待ち受けポート番号を設定する。

app.listen(8888)

6. 接続受付を行う。Ctrl-Cしない限りは応答が返らない。

 IOLoop.current().start()

 起動するとAPIドキュメントが自動生成される。また、応答形式はapplication/jsonとなり、{"status":{実行結果} "data":{get/postのreturn内容}となる

 

ソース全体

 /home/pythonSample/tornado-json
|--helloworld
|  |--api.py
|  |--__init__.py
|--server.py


<server.py>
from tornado.ioloop import IOLoop
from tornado_json.routes import get_routes
from tornado_json.application import Application
import json

def make_app():
    import helloworld
    routes = get_routes(helloworld)
    print("Routes\n=======\n\n" +
           json.dumps([(url, repr(rh)) for url, rh in routes], indent=2)
          )
    settings = {"debug":True }
    return Application(routes=routes, settings=settings, generate_docs=True)

if __name__ == "__main__":
   app = make_app()
   app.listen(8888)
   IOLoop.instance().start()

<api.py>
from tornado_json.requesthandlers import APIHandler
from tornado_json import schema

class HelloWorldHandler(APIHandler):

    @schema.validate(
        output_schema={"type":"string"},
    )
    def get(self):
        return "Hello world!"

 

URLの決定

  {パッケージ名}/{モジュール名}/{Handler名}となる。Handler名からはHandlerの文字列は取り除かれてすべて小文字となる。 パッケージ名はget_routes()で指定したパッケージ配下にパッケージがある場合に適用される。
 上記の例ではURLは「/api/helloworld」となる

URLパラメータの取得

  get,postに引数を追加した場合、自動的にURLパラメータを受け取る形になる。パターンはw+の形式となる。引数のパターンはHandler名の後に追加される

 class UrlParamHandler(APIHandler):
    def get(self, fname, lname):
 → /api/urlparam/(?P<fname>[a-zA-Z0-9_\\-]+)/(?P<lname>[a-zA-Z0-9_\\-]+)/?$

URLの指定

 Handler内に__url_names__を定義すると、Handler名に相当するところが置き換わる。また、Handler内に__urls__を定義するとURLが定義した値通りとなる。
 ただし、__urls__を定義した場合、__url_names__ = [] としないと自動生成されるURLも登録されてしまう。

クエリの取得

 tornado.webと同様にget_query_argumentを使用する。

POSTパラメータの取得

 jsonで受け取ることが前提となる。値は、self.body["プロパティ名"]で取得する。そのためにはschema.validateでinput_schemaを定義する必要がある。定義しないと、self.bodyで値は受け取れない。

 @schema.validate(
     input_schema={
          "type": "object",
          "properties": {
             "title": {"type": "string"},
             "body":  {"type": "string"},
             "index": {"type": "number"},
           },
           "required": ["title", "body"]
     },
     output_schema={
          "type": "object",
          "properties": {
              "message": {"type": "string"},
          }
     },
 )
 def post(self):
     return {
       "message": "{} was posted.".format(self.body["title"]),
     }

出力処理について

 各Handlerのget/postの返り値がそのまま応答値となるが、デフォルトでは以下のように加工して返している。

{ "status":"success", "data":{return値の内容}}

 これを変更したい場合は、APIHandler.success()をオーバライドする。

バリデーション

 @schema.validateを定義しておけば、指定した定義に基づいたバリデーションの実行が行われる。スキーマ定義には存在しないプロパティに関してはチェックされない(余分なパラメータがあってもエラーとはしない)。
 渡すスキーマ定義を別ファイルに定義する場合は、それを読み込むための仕組みが必要(Tornado-JSONにはない)。

エラー処理のハンドリング

 バリデーションでエラーとなった場合、デフォルトでは以下の形式で応答が返る。

 HTTP STATUS 400 Bad Request
 { "status":"fail", "data":{バリデーションエラーメッセージ}}

 また、get/post内で例外がスローされた場合はデフォルトでは以下の形式で応答が返る。

 { "status":"error", "code":{例外に応じたHTTP STATUS(通常は500)}, "message":{HTTP STATUSに応じた内容(Internal Server Errorなど), "data":{例外の内容(debug=Trueで起動した時のみ)}}

 dataに関しては、例外が属性:log_messageを持てばその内容が、持たない場合はstr()で例外を変換した内容が入る。tornado.web.HTTPErrorがこの属性を持っている。
 
 上記の応答内容を変更したい場合は、APIHandler.fail()とAPIHandler.error()をオーバライドする。
 APIHandler.fail()は例外としてjsonschema.ValidationErrorかtornado_json.exceptions.APIErrorがスローされた場合に呼ばれる。
 APIHandler.error()はそれ以外の例外がスローされた場合に呼ばれる。tornado.web.HTTPErrorはこちらで処理されることになる。
 独自で例外を作成する場合も、APIErrorを継承した方が例外の応答処理が楽に作れる。
 
 また、404応答を独自に返したい場合、Handlerを作成してApplicationのオプションdefault_handler_classで作成したHandlerを指定すればよい。特に指定しない場合はErrorHandlerが指定される。が、応答がtext/plainとなりjson形式ではないため変更するのが望ましい。

参考

Tornado-JSON — Tornado-JSON 1.2.2 documentation

上記のサンプルコードは以下にあります。

github.com