PyramidでSwaggerを使う

Swaggerは公式サイトではこのように説明されています。

Swagger is a simple yet powerful representation of your RESTful API. With the largest ecosystem of API tooling on the planet, thousands of developers are supporting Swagger in almost every modern programming language and deployment environment. With a Swagger-enabled API, you get interactive documentation, client SDK generation and discoverability.

私なりに意訳すると「SwaggerはRESTful APIのシンプルで強力な表現で、ほぼすべてのモダンなプログラミング言語をサポートしています。Swaggerにより、ドキュメントとクライアントのSDKを生成することができます。」です。Swaggerのフォーマットに従って、YAMLまたはJSONでAPIを定義します。

このSwaggerでAPIを定義し、そのAPIをPyramidで提供してみます。

前提

今回の環境です。

  • Python 3.4.3
  • Pyramid 1.7
  • pyramid-swagger 2.3.0rc2

pyramid-swaggerの2.3.0rc2と、rc版を利用したのは、リリースノートによると2.3系からYAMLをサポートしているからです。ただし、YAMLでは動作しなかったので、JSONを今回は使用します。

作るのはToDoアプリケーションです。ToDoの一覧、ToDoの追加、ToDoの削除の機能があり、それをJSONで入出力します。

pcreate -s starter でプロジェクトを作り終わったところから始めます。

pyramid-swaggerのインストール

PyPIにあるのでpipコマンド経由でインストールできます。

pip3 install 'pyramid-swagger==2.3.0rc2'

SwaggerでAPIの定義

JSONとYAMLならYAMLで記述したいところですが、上記の理由により今回はJSONです。ただし、SwaggerのYAMLをJSONに直すのは難しくないはずなので、YAMLで書き、JSONに変換し読ませるのが良さそうです。

このJSONをpcreateで出力したプロジェクトのsetup.pyのあるディレクトリから見て、 api_docs/swagger.json に保存します。

.
├── api_docs
│   └── swagger.json
├── setup.py
{
    "swagger": "2.0",
    "info": {
        "title": "swagru",
        "description": "SwaggerをPyramidと試す。",
        "termsOfService": "http://blog.varwww.com",
        "contact": {
            "name": "Motoki Naruse",
            "url": "http://blog.varwww.com"
        },
        "license": {
            "name": "Apache 2.0",
            "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
        },
        "version": "1.0.0"
    },
    "host": "swagru",
    "schemes": [
        "http"
    ],
    "basePath": "/v1",
    "produces": [
        "application/json"
    ],
    "paths": {
        "/todo": {
            "get": {
                "description": "ToDoの一覧を返す。",
                "parameters": [],
                "tags": [
                    "ToDo"
                ],
                "responses": {
                    "200": {
                        "description": "ToDoのリスト",
                        "schema": {
                            "type": "array",
                            "items": {
                                "$ref": "#/definitions/Todo"
                            }
                        }
                    }
                }
            },
            "post": {
                "description": "ToDoを追加する。",
                "parameters": [
                    {
                        "name": "todo",
                        "in": "body",
                        "description": "追加するToDo。",
                        "required": true,
                        "schema": {
                            "$ref": "#/definitions/NewTodo"
                        }
                    }
                ],
                "tags": [
                    "ToDo"
                ],
                "responses": {
                    "201": {
                        "description": "追加成功。",
                        "schema": {
                            "$ref": "#/definitions/Todo"
                        }
                    }
                }
            }
        },
        "/todo/{id}": {
            "delete": {
                "summary": "ToDoを消す。",
                "description": "ToDoを消す。",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "description": "ID of pet to delete",
                        "required": true,
                        "type": "integer"
                    }
                ],
                "tags": [
                    "ToDo"
                ],
                "responses": {
                    "204": {
                        "description": "削除成功。"
                    }
                }
            }
        }
    },
    "definitions": {
        "Todo": {
            "type": "object",
            "properties": {
                "id": {
                    "type": "integer",
                    "description": "ToDoの一意な識別子。"
                },
                "text": {
                    "type": "string",
                    "description": "ToDoの内容。"
                }
            }
        },
        "NewTodo": {
            "type": "object",
            "properties": {
                "text": {
                    "type": "string",
                    "description": "ToDoの内容。"
                }
            }
        }
    }
}

Routeの定義

config.add_route('list_todo', '/v1/todo', request_method='GET')
config.add_route('add_todo', '/v1/todo', request_method='POST')
config.add_route('delete_todo', '/v1/todo/{id}', request_method='DELETE')

Viewの定義

試すだけなので、データベースは用意していません。pserveを止めるたびにToDoは消えてしまいます。

import pyramid


todo = {}


@pyramid.view.view_config(route_name='list_todo', renderer='json')
def list_todo(request: pyramid.request.Request) -> dict:
    return [
        {
            'id': id,
            'text': text
        } for id, text in todo.items()
    ]


@pyramid.view.view_config(route_name='add_todo', renderer='json')
def add_todo(request: pyramid.request.Request) -> dict:
    request.response.status = 201
    id = max(todo.keys()) + 1 if todo else 1
    text = request.swagger_data['todo']['text']
    todo[id] = text
    return {
        'id': id,
        'text': text
    }


@pyramid.view.view_config(route_name='delete_todo')
def delete_todo(request: pyramid.request.Request) -> pyramid.response.Response:
    del todo[request.swagger_data['id']]
    request.response.status = 204
    return request.response

pyramid-swaggerの読み込み

development.inipyramid.includes に加えます。

pyramid.includes =
    pyramid_swagger

感想

FlaskなどのようにAPIサーバのViewを自動で出力してもらえるともっと便利になると思います。自分でViewを定義していては手間ですからね。ただ、それでも、入力や出力についてSwaggerのJSONに従っていなければエラーにしてくれるので、APIドキュメントと実装の乖離を防ぐことはできそうです。Pyramidようにも自動生成できればより強く防げるはずですけど。

コメント

2015 - 2017 (c) 成瀬基樹