最近のコマンドラインからのJSONのAPIのPOSTなどの仕方

手元の環境でweb apiを叩く処理をしたい時にどうしているかというメモ。

利用しているもの

本当はpipで新たにインストールしなければ行けないコマンドに頼らない形が良いけれど。curlが辛くなったというのと。curlに飽きたというのでhttpieを使っている。

pip install httpie

apiの指定

例えば以下のようなAPIを呼びたいとする。

POST /pets
{
  "id": <number>,
  "name": <string>,
  "tag": <string>
}

JSONの文字列をコマンドライン上で作るのは辛い*1。なので以下のように別のファイルに書くことにしている

src/pet0.json

{
    "id": 1,
    "name": "foo",
    "tag": "base"
}

httpieを使う時のコマンドとrequest内容は以下の様な感じ。

$ cat src/pet0.json | http --json --verbose post http://localhost:4444/pets
POST /pets HTTP/1.1
Accept: application/json, */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 48
Content-Type: application/json
Host: localhost:4444
User-Agent: HTTPie/0.9.6

{
    "id": 1,
    "name": "foo",
    "tag": "base"
}

引数の設定

ところどころ引数を変えたい場合がある。今のところはpythonの標準ライブラリの範囲で問題ない。format関数はjsonと相性が良くないので % を使っている。外部ライブラリに依存しちゃって良いのならmakoやjinja2を使ったほうが便利。

$ python ./bin/getjson.py --config config.json src/pet1.json | http --json --verbose post http://localhost:4444/pets
POST /pets HTTP/1.1
Accept: application/json, */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 48
Content-Type: application/json
Host: localhost:4444
User-Agent: HTTPie/0.9.6

{
    "id": 1,
    "name": "foo",
    "tag": "base"
}

src/pet1.json

{
    "id": "%(petId)s",
    "name": "%(petName)s",
    "tag": "base"
}

config.json

{
    "petId": 1,
    "petName": "foo"
}

bin/getjson.py

import sys
import json


def main():
    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("-c", "--config", required=True)
    parser.add_argument("file")
    args = parser.parse_args()
    dump(args.config, args.file)


def dump(config, file):
    with open(config) as rf:
        conf = json.load(rf)

    with open(file) as rf:
        template = rf.read()
        data = template % conf
        print(json.dumps(json.loads(data), indent=2, sort_keys=True, ensure_ascii=False))

if __name__ == "__main__":
    main()

引数のオプションなどを覚えたくない

細かい引数をいちいち覚えておくのが面倒なのでMakefileを書く。何か他のものを使ったほうが楽な気がしないこともない感じ。

URL = http://localhost:4444
GETJSON = python ./bin/getjson.py --config config.json

api_create_pet:
   @${GETJSON} src/pet1.json | http --json --verbose post ${URL}/pets
$ make api_create_pet
POST /pets HTTP/1.1
Accept: application/json, */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 48
Content-Type: application/json
Host: localhost:4444
User-Agent: HTTPie/0.9.6

{
    "id": 1,
    "name": "foo",
    "tag": "base"
}

ちなみにresponseをそのままjsonとして受け取りたい場合には --verboseを外した方が良い。

Authorizatioin ヘッダーなどが必要な場合

jwtなどを使っていてAuthorization ヘッダーが必要な場合にはテキトウに生成した結果を先述のconfig.jsonに埋め込んで cat config.json | jq <path> -r を使って取り出している。

get_auth_header:
    @<generate header> --clietId `cat config.json | jq .clientId -r` --salt `cat config.json | jq .clientSalt -r`
make get_auth_header|pbcopy  # config.jsonに保存

authorization headerを付ける場合

AUTH_HEADER = Authorization:"Bearer `cat config.json | jq .token -r`"

api_create_pet:
   @${GETJSON} src/pet1.json | http --json --verbose post ${URL}/pets ${AUTH_HEADER}

おわりに

本当は最初から入っているコマンドだけで完結しているだとか。他の人とも共有しやすい(e.g. postman使う)ほうが良いのかもしれないけれど。今はとりあえずこういう感じ。

細かい補足

format関数はjsonと相性が悪い理由などの補足

補足1-1 format関数がjsonと相性が悪い理由

例えばjsonのファイルを以下の様に書かなくてはいけない。

{{
    "id": "{petId}",
    "name": "{petName}",
    "tag": "base"
}}

補足1-2 %の問題

ネストした辞書をconfigに持てない。

# formatはok
"@{foo[bar]}@".format(**{"foo": {"bar": "boo"}})  # => "@boo@"

# % はng
"@(foo[bar])@" % ({"foo": {"bar": "boo"}})  # => error

なので現状は諦めてフラットなjsonにデータを持っている。

補足1-3 jinja2やmakoの場合

これはどちらも大丈夫。

mako

from mako.template import Template
Template("@${foo['bar']}@").render(**{"foo": {"bar": "boo"}})  # => "@boo@"

Template("""{
    "id": "${petId}",
    "name": "${petName}",
    "tag": "base"
}""").render(petId=1, petName="foo")

jinja2

from jinja2 import Template
Template("@{{foo['bar']}}@").render(**{"foo": {"bar": "boo"}})  # => "@boo@"

Template("""{
    "id": "{{petId}}",
    "name": "{{petName}}",
    "tag": "base"
}""").render(petId=1, petName="foo")

補足2 apiのswagger

こういうapi setを想定していた

swagger: '2.0'
info:
  version: '1.0.0'
  title: Swagger Petstore (Simple)
  description: A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification
  termsOfService: http://helloreverb.com/terms/
  contact:
    name: Swagger API team
    email: foo@example.com
    url: http://swagger.io
  license:
    name: MIT
    url: http://opensource.org/licenses/MIT
host: localhost:4444
basePath: /
schemes:
  - http
consumes:
  - application/json
produces:
  - application/json
paths:
  /pets:
    get:
      description: Returns all pets from the system that the user has access to
      operationId: findPets
      produces:
        - application/json
        - application/xml
        - text/xml
        - text/html
      parameters:
        - name: tags
          in: query
          description: tags to filter by
          required: false
          type: array
          items:
            type: string
          collectionFormat: csv
        - name: limit
          in: query
          description: maximum number of results to return
          required: false
          type: integer
          format: int32
      responses:
        '200':
          description: pet response
          schema:
            type: array
            items:
              $ref: '#/definitions/pet'
        default:
          description: unexpected error
          schema:
            $ref: '#/definitions/errorModel'
    post:
      description: Creates a new pet in the store.  Duplicates are allowed
      operationId: addPet
      produces:
        - application/json
      parameters:
        - name: pet
          in: body
          description: Pet to add to the store
          required: true
          schema:
            $ref: '#/definitions/newPet'
      responses:
        '200':
          description: pet response
          schema:
            $ref: '#/definitions/pet'
        default:
          description: unexpected error
          schema:
            $ref: '#/definitions/errorModel'
definitions:
  pet:
    type: object
    required:
      - id
      - name
    properties:
      id:
        type: integer
        format: int64
      name:
        type: string
      tag:
        type: string
  newPet:
    type: object
    required:
      - name
    properties:
      id:
        type: integer
        format: int64
      name:
        type: string
      tag:
        type: string
  errorModel:
    type: object
    required:
      - code
      - message
    properties:
      code:
        type: integer
        format: int32
      message:
        type: string

*1:joを使っても良いという話はあるかもしれない