apistarのJSON response でdatetimeを含んだdictを返しても大丈夫なようにする
apistarというdjango rest frameworkなどを作っている人たちが作っているフレームワークがある。まだ開発中。既存のpythonのフレームワークとは異なりapiサーバーの実装の方をデフォルトとしているのでちょっとしたものを作る分には便利。
このapistarでよくあるjson.dumpsできないdictをそのままJSONレスポンスにする方法を調べたのでメモ。
前提
$ pip freeze | grep apistar apistar==0.5.25
開発が進むとともにこのメモの内容は古くなる可能性がある
defaultではエラーになる
例えばテキトウにdatetime.now()を含んだdictを返すAPIを作ってみる。
from apistar import App, Route from datetime import datetime def hello() -> dict: return { "hello": "world", "now": datetime.now(), } if __name__ == "__main__": routes = [ Route("/", "GET", hello), ] app = App(routes=routes) app.serve('127.0.0.1', 5000, debug=True)
エラーになる。
File "/usr/lib/python3.6/_weakrefset.py", line 75, in __contains__ return wr in self.data RecursionError: maximum recursion depth exceeded in comparison
これをどうにかしたい。
現状ではrendering方法をカスタマイズする方法はそんざいしていない
現状ではrendering方法をカスタマイズする方法はそんざいしていない。実際中のコードを見てみると以下のようになっている。
class JSONResponse(Response): media_type = 'application/json' charset = None options = { 'ensure_ascii': False, 'allow_nan': False, 'indent': None, 'separators': (',', ':'), } def render(self, content: typing.Any) -> bytes: options = {'default': self.default} options.update(self.options) return json.dumps(content, **options).encode('utf-8') def default(self, obj: typing.Any) -> typing.Any: if isinstance(obj, types.Type): return dict(obj) error = "Object of type '%s' is not JSON serializable." return TypeError(error % type(obj).__name__)
json.dumpsのdefaultにJSONResponse.default
が渡されてはいるものの。dict()
を試すということしかしていない。そんなわけでまだ対応していないようだった。
work-around
仕方がないので以下の様なコードを書く。ついでにtypingも使っておく。
import typing from apistar import App, Route from apistar.http import Response, HTMLResponse, JSONResponse from apistar.server.components import ReturnValue from datetime import datetime class MyApp(App): def render_response(self, return_value: ReturnValue) -> Response: if isinstance(return_value, Response): return return_value elif isinstance(return_value, str): return HTMLResponse(return_value) return MyJSONResponse(return_value) class MyJSONResponse(JSONResponse): def default(self, obj: typing.Any) -> typing.Any: if isinstance(obj, datetime): return obj.isoformat() return super().default(obj) def hello() -> dict: return { "hello": "world", "now": datetime.now(), } if __name__ == "__main__": routes = [ Route("/", "GET", hello), ] app = App(routes=routes) app.serve('127.0.0.1', 5000, debug=True)
今度は通る。
$ python app.py & $ http -b :5000/ { "hello": "world", "now": "2018-05-26T15:02:56.854735" }
余談
とは言え、色々apistarの機能は開発中で変わることもありうる。ついでに、swagger(openAPI)に対応しようとしたりgraphqlもサポートできたら良いねみたいなissueもあったりする通り、型をどこかで定義してそこからのマッピングをしようという方向に考えているように見える。
実際にtypingを使った型を定義してそちらを使って自動でJSONに変換しようというような例がドキュメントにも存在している。なのでそちらの方向に進んでいくような気もしている。
makefileのforeachのハマりどころ
タスクランナーとしてのmakeのN回目。あんまり深追いしたくはないという気持ちもありつつ。
以前書いたようにmakefile中ではbashの関数などの定義ができない。そんなわけでdefineとcallを使う。これはこれで便利。ただforeachまで使おうとするとちょっとハマるかもというポイントがあるのでメモ。
define,callを使って処理をまとめる
# この定義はforeachを使う所で上手くいかなくなるので注意(良くない例) define F echo $(1) echo $(1) endef f0: $(call F,foo) $(call F,bar)
これは以下の様に展開される。
$ make -n f0 echo foo echo foo echo bar echo bar
なるほど。良い。
foreachを使いたい
さて、Makefileで複数の対象を候補に同じ処理を呼びたいということもある。つまるところ対象としてlistを指定したい。このようなときにはforeachを使う。使うのだけれど。これに少しだけハマりどころが存在する(こういうところがあるのでMakeの知識はバッドノウハウっぽい。本当は代替のタスクランナーがあれば乗り換えたい。ただしできればインストールにパッケージマネージャが不要なものが良い(LLなどはこのあたりで候補から外れる))。
以下の様にリスト(xs)を用意して、先程定義したFをそのリストに対して使おうとしてみる。
# この定義はforeachを使う所で上手くいかなくなるので注意(良くない例) define F echo $(1) echo $(1) endef xs := foo bar f1: $(foreach x,$(xs),$(call F,$(x)))
-n
で実際に実行されるスクリプトを見てみると、一部期待通りではない形になっている。もちろん当然ではあるけれど。このようなコードではfooでの末尾を実行したタイミングで不用意に引数としてbarの先頭行が渡されるということになり。エラーが出てしまう。
$ make f1 -n echo foo echo foo echo bar echo bar
Makefileのdefineはテキストを定義するというだけの意味なので。endefの手前までの部分、つまるところ改行を含まない定義がforeachによって呼ばれるのでダメ。
期待通りに書くには以下の様にする必要がある。
# こちらは期待通りに動く define F echo $(1) echo $(1) endef xs := foo bar f2: $(foreach x,$(xs),$(call F,$(x)))
最後に1行改行が必要。こうすると上手くいく。
$ make f2 -n echo foo echo foo echo bar echo bar
foreachが使えるようになるとできること
ちなみにforeachが使えるようになるとできることが色々ある。wildcardとbasename,addsuffixあたりを組み合わせると便利。
(あんまりmakeのことを覚えても仕方がないというところもあるので$(shell ...)
やバッククォートによるコマンドの実行を使った形の方が共有はしやすいかもしれない)
例えば、setup.py
を持つものをpython packageだとすると。これらの全部のテストを実行するには以下で良い。
define testT (cd $(dir $(1)) && python setup.py -q test) endef testall: $(foreach p,$(wildcard */setup.py),$(call testT,$(p)))
実際に実行してみる
$ make -n (cd foo/ && python setup.py -q test) (cd bar/ && python setup.py -q test) $ make (cd foo/ && python setup.py -q test) ---------------------------------------------------------------------- Ran 0 tests in 0.000s OK (cd bar/ && python setup.py -q test) ---------------------------------------------------------------------- Ran 0 tests in 0.000s OK
注意点として、makeでのコマンドは1行毎に違うシェルで実行されるので(正確な表現ではないけれど)以下ではダメ。
define testT cd $(dir $(1)) python setup.py -q test # cdする前の位置で実行される cd .. endef