pyramid-swagger-routerと一緒にtoyboxのswaggerのvalidationを使ってみる

相も変わらずtoyboxというリポジトリで作業していた。昨日試しに作ってみたswaggerのinput,outputのvalidationを行うものを昔作っていた pyramid-swagger-router と一緒に使ってみるようにしてみた。ココまで来るとそろそろswaggerに対するnormalizer + iteratorみたいなものが欲しくなってくる。

動くサンプルは ここ

使い方

だいたい以下のようなMakefileを使うという感じで察して欲しい(yapfのコメントアウトはASTのmergeに使うredbaronというpackageのバグを踏んでしまっていたので。yapfはgofmtのpython版みたいなもの)。

gen: spec router schema

# fmt:
#  yapf -r -i --style='{based_on_style: chromium, indent_width: 4}' app

spec:
  cp ../swagger/swagger.yml .
  gsed -i 's/operationId: /operationId: views./g' swagger.yml

router:
  pyramid-swagger-router --driver=./driver.py:Driver --logging=DEBUG ./swagger.yml app

schema:
  swagger-marshmallow-codegen --full --logging=DEBUG ./swagger.yml > app/schema.py

run:
  PYTHONPATH=. python app/__init__.py

このようにするとだいたい以下のようなコードが生成される。 使うswagger specは 昨日のものと同じもの

routes.py

def includeme_swagger_router(config):
    config.add_route('views', '/')
    config.add_route('views1', '/add')
    config.add_route('views2', '/dateadd')
    config.scan('.views')


def includeme(config):
    config.include(includeme_swagger_router)

views.py

from pyramid.view import (
    view_config
)
from . import (
    schema
)
from toybox.swagger import (
    withswagger
)


@view_config(decorator=withswagger(schema.Input, schema.Output), renderer='vjson', request_method='GET', route_name='views')
def hello(context, request):
    """

    request.GET:

        * 'name'  -  `{"type": "string", "example": "Ada", "default": "Friend"}`
    """
    return {}


@view_config(decorator=withswagger(schema.AddInput, schema.AddOutput), renderer='vjson', request_method='POST', route_name='views1')
def add(context, request):
    """


    request.json_body:

    ```
        {
          "type": "object",
          "properties": {
            "x": {
              "type": "integer"
            },
            "y": {
              "type": "integer"
            }
          },
          "required": [
            "x",
            "y"
          ]
        }
    ```
    """
    return {}


@view_config(decorator=withswagger(schema.DateaddInput, schema.DateaddOutput), renderer='vjson', request_method='POST', route_name='views2')
def dateadd(context, request):
    """


    request.json_body:

    ```
        {
          "type": "object",
          "properties": {
            "value": {
              "type": "string",
              "format": "date"
            },
            "addend": {
              "minimum": 1,
              "type": "integer"
            },
            "unit": {
              "type": "string",
              "default": "days",
              "enum": [
                "days",
                "minutes"
              ]
            }
          },
          "required": [
            "addend"
          ]
        }
    ```
    """
    return {}

init.pyは自分で書かなければダメ。

# -*- coding:utf-8 -*-
from toybox.simpleapi import run


def includeme(config):
    config.include("toybox.swagger")
    config.include("app.routes")
    config.scan("app.views")


if __name__ == "__main__":
    run.include(includeme)
    run(port=5001)

以下の事が自動で行われる

  • routingの定義
  • viewの内部のformat validation

もちろんroutingができるだけなので内部のviewは自分で実装してあげないとダメ。

diff --git a/examples/swagger2/app/views.py b/examples/swagger2/app/views.py
index a531b8c..85b5a96 100644
--- a/examples/swagger2/app/views.py
+++ b/examples/swagger2/app/views.py
@@ -1,3 +1,4 @@
+from datetime import datetime, timedelta
 from pyramid.view import (
     view_config
 )
@@ -17,7 +18,7 @@ def hello(context, request):
 
         * 'name'  -  `{"type": "string", "example": "Ada", "default": "Friend"}`
     """
-    return {}
+    return {'message': 'Welcome, {}!'.format(request.GET["name"])}
 
 
 @view_config(decorator=withswagger(schema.AddInput, schema.AddOutput), renderer='vjson', request_method='POST', route_name='views1')
@@ -45,7 +46,9 @@ def add(context, request):
         }
     ```
     """
-    return {}
+    x = request.json["x"]
+    y = request.json["y"]
+    return {"result": x + y}
 
 
 @view_config(decorator=withswagger(schema.DateaddInput, schema.DateaddOutput), renderer='vjson', request_method='POST', route_name='views2')
@@ -82,4 +85,13 @@ def dateadd(context, request):
         }
     ```
     """
-    return {}
\ No newline at end of file
+    value = request.json["value"]
+    addend = request.json["addend"]
+    unit = request.json["unit"]
+    value = value or datetime.utcnow()
+    if unit == 'minutes':
+        delta = timedelta(minutes=addend)
+    else:
+        delta = timedelta(days=addend)
+    result = value + delta
+    return {'result': result}

pyramid-swagger-routerについて

昔記事書いていた

http://pod.hatenablog.com/entry/2017/01/03/202551

生成したmarshmallowのschemaをwrapしてpyramidから使えるようにしてみた

swaggerからmarshmallowのschemaを生成する機能は昔から作っていて、デフォルトでは definitions 部分だけしか見ないのだけれど。--full というオプションをつけると paths 以下の parametersresponses も見るようになっている。ここで生成したschemaをpyramidから使えるようにした。まだpypiにはあげていない。

だいたい以下の様な手順で使う。

$ pip install swagger-marshmallow-codegen
$ swagger-marshmallow-codegen swagger.yml --full > schema.py

あとは以下の様な感じでコードを書けばOK。以下が重要。

  • config.include("toybox.swagger")
  • withswaggerで生成されたschemaを渡したdecoratorをつける
from datetime import datetime, timedelta
from toybox.simpleapi import simple_view, run
from toybox.swagger import withswagger
import schema  # ./schema.py


@simple_view("/", decorator=withswagger(input=schema.Input, output=schema.Output))
def hello(request):
    return {'message': 'Welcome, {}!'.format(request.GET["name"])}


@simple_view("/add", request_method="POST", decorator=withswagger(input=schema.AddInput, output=schema.AddOutput))
def add(request):
    x = request.json["x"]
    y = request.json["y"]
    return {"result": x + y}


@simple_view("/dateadd", request_method="POST", decorator=withswagger(input=schema.DateaddInput, output=schema.DateaddOutput))
def dateadd(request):
    value = request.json["value"]
    addend = request.json["addend"]
    unit = request.json["unit"]
    value = value or datetime.utcnow()
    if unit == 'minutes':
        delta = timedelta(minutes=addend)
    else:
        delta = timedelta(days=addend)
    result = value + delta
    return {'result': result}


if __name__ == "__main__":
    import logging
    logging.basicConfig(level=logging.DEBUG)
    run.include("toybox.swagger")
    run(port=5001)

viewの内部では全てswaggerのspecを満たしたrequsestであることが保証されている。ダメだった場合にはHTTPBadRequestが返る。

例えば以下の様な感じ

$ http POST :5001/add x=10 y=20
HTTP/1.0 200 OK
Content-Length: 14
Content-Type: application/json
Date: Sat, 18 Mar 2017 18:24:52 GMT
Server: WSGIServer/0.2 CPython/3.5.2

{
    "result": 30
}

$ http POST :5001/add x=10
HTTP/1.0 400 Bad Request
Content-Length: 107
Content-Type: application/json
Date: Sat, 18 Mar 2017 18:24:55 GMT
Server: WSGIServer/0.2 CPython/3.5.2

{
    "code": "400 Bad Request",
    "message": {
        "y": [
            "Missing data for required field."
        ]
    },
    "title": "Bad Request"
}

$ http POST :5001/add
HTTP/1.0 400 Bad Request
Content-Length: 214
Content-Type: application/json
Date: Sat, 18 Mar 2017 18:24:57 GMT
Server: WSGIServer/0.2 CPython/3.5.2

{
    "code": "400 Bad Request",
    "message": "The server could not comply with the request since it is either malformed or otherwise incorrect.\n\n\nExpecting value: line 1 column 1 (char 0)\n\n",
    "title": "Bad Request"
}

viewへのinputとして渡せるのはswaggerと同様に以下。

  • path – /foo/{name} みたいなやつ
  • query – /?foo=bar みたいなやつ
  • header – request header
  • body – REST APIなどでjsonをpostしたときのやつ
  • form – 普通のPOST

example全体は以下のリンク先。

https://github.com/podhmo/toybox/tree/master/examples/swagger

どうでも良いこと

ところでmarshmallowのvalidationがload(deserialization)時にだけ掛かるということに気づき衝撃を受けた。しょうがないのでserialize後deserializeするみたいなコードになっている。