最近のコマンドラインからの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の場合
これはどちらも大丈夫。
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を使っても良いという話はあるかもしれない