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

kamidanaにsnakecase,camelCaseの変換や複数形に変換する機能を追加した

kamidanaにsnakecase,camelCaseの変換や複数形に変換する機能を追加した。

実行には以下のようにadditionalsの指定が必要。

$ kamidana -a kamidana.additionals.naming hello.jinja2

snake_case,camelCase

から

時折、ファイルの種類によってsnake_caseだったりcamelCaseだったりkebab-caseだったりするので。変換したくなる。

例えば以下の様にしてあげると

jinja2テンプレート

camelCase,snakecase,kebabcase

- fooBarBoo -> {{"fooBarBoo"}}
- fooBarBoo|snakecase -> {{"fooBarBoo"|snakecase}}
- fooBarBoo|kebabcase -> {{"fooBarBoo"|kebabcase}}

良い感じにfilterで変換した結果を手にすることができるようになる。

出力結果

camelCase,snakecase,kebabcase

- fooBarBoo -> fooBarBoo
- fooBarBoo|snakecase -> foo_bar_boo
- fooBarBoo|kebabcase -> foo-bar-boo

単数形,複数形

名詞を複数形にしたりしたかったりすることがある。

jinja2テンプレート

singular, plurals

- days|singularize -> {{"days"|singularize}}
- day|pluralize -> {{"day"|pluralize}}

- categories|singularize -> {{"categories"|singularize}}
- category|pluralize -> {{"category"|pluralize}}

これも同様にやってくれるようになった。

出力結果

singular, plurals

- days|singularize -> day
- day|pluralize -> days

- categories|singularize -> category
- category|pluralize -> categories

go-bindataの代わりにstatikを使うことにした

以前はgo-bindataを使っていたのだけれど。そういえば、作者がアカウント削除したあとに他の人がリポジトリ作成したみたいなことがあったな−。ということを思い出したりしたので代替品を探してみることにした。

statikを使う。statikはこちら。

正直な所どれでも良いのだけれど。必要なパッケージのインストールが以下だけで済むのが気に入った。

go get -v github.com/rakyll/statik

使いかた自体は、statikコマンドが使えるようになるのでREADMEの通りに使えば良い。

例えばconfig以下を埋め込むなら以下のようにする(statikというディレクトリが生成される)。

$ statik -src config -f

http.FileSystem

http.FileSystemインターフェイスを実装したものを通じてアクセスする。これは以下のようなインターフェイス

// A FileSystem implements access to a collection of named files.
// The elements in a file path are separated by slash ('/', U+002F)
// characters, regardless of host operating system convention.
type FileSystem interface {
    Open(name string) (File, error)
}

// A File is returned by a FileSystem's Open method and can be
// served by the FileServer implementation.
//
// The methods should behave the same as those on an *os.File.
type File interface {
    io.Closer
    io.Reader
    io.Seeker
    Readdir(count int) ([]os.FileInfo, error)
    Stat() (os.FileInfo, error)
}

すっごく薄いvfs(virtual file system)と考えてみても良いかもしれない。

import "github.com/rakyll/statik/fs"
import __ "./statik" // まじめにやるならabsolute path

FS, _ := fs.New()
f, _ := FS.Open("/message.txt") // 先程-srcで指定したconfigディレクトリのmessage.txtが参照できる
defer f.Close()
io.Copy(os.Stdout, f)

ところで、個人的には http.FileSystem のようなインターフェイスは苦手で、これをmapと似たようなものと捉えるなら、どのようなkeyでアクセスできるかの一覧が欲しい。別な表現をするなら、ディレクトリに対するlsないしはfindのような操作が行いたい。

fs.New()の返す値は、内部的には以下の様な定義だった。

type statikFS struct {
    files map[string]file
}

unexportedなfieldに値が格納されているので、reflectを使って無理やり取り出す。以下の様な感じにすれば良さそう。 (ファイルのパスだけ知りたいのならkだけで十分。中のbytesを取り出したいならMapIndexなどを使って取り出している処理が必要になる)

import "github.com/rakyll/statik/fs"
import __ "./statik" // まじめにやるならabsolute path

FS, _ := fs.New()

rm := reflect.ValueOf(FS).Elem().FieldByName("files")
for _, k := range rm.MapKeys() {
    v := string(rm.MapIndex(k).FieldByName("data").Bytes())
    fmt.Println(k, v)
}

// /message.txt hello

まぁ、そんなわけで、config/message.txt"/message.txt" でアクセスできる。

追記

似たような記事が既にあった。こちらはgo-assetsだけれど。

(ところで参照先の記事はなんで json.Decoder などを使わず、json.Unmarshal()bytes.Buffer 越しに使おうとしているのだろう?)

emacsで正規表現で置換する場合に入力する値のメモ

emacs正規表現で置換する場合のメモ。ついついescapeなどの対応関係を忘れてしまうので。

int(xxx)xxxに変換する

M-x replace-stringでminibufferで入力する場合

int(\([^)]+\)) -> \1

ちなみに文字列で渡す場合には全部にescapeが要る。

(replace-regexp "int(\\([^)]+\\)" "\\1")

M-x re-builderC-c C-wで保存できるのは後者の形式

goでは自分自身を返すメソッドは定義しないほうが良いのでは?という話。

最近色々コードを書いていた中で、goでは自分自身を返すメソッドは定義しないほうが良いのでは?という思いを抱くことがあった。自分自身を抱えすメソッドを定義するということを一般に悪いとする主義主張の人もいたりもするけれど。そこまで強硬に主張したいわけではなく。あくまでgoを書く上ではだけの話。

自分自身を返すメソッド?

具体的には、ElasticSearchにアクセスする必要があり以下のライブラリを使っていた。

このライブラリで対象のindexに対して検索を行う機能がありそれには以下のようなstructを使う。(正確に言うと10000件以内の検索であればSearchServiceで十分だがそれ以上の件数であればScrollServiceを使う必要がある。いわゆるcursor的なもの)

// ScrollService iterates over pages of search results from Elasticsearch.
type ScrollService struct {
    client            *Client
    retrier           Retrier
    indices           []string
    types             []string
    keepAlive         string
    body              interface{}
    ss                *SearchSource
    size              *int
    pretty            bool
    routing           string
    preference        string
    ignoreUnavailable *bool
    allowNoIndices    *bool
    expandWildcards   string

    mu       sync.RWMutex
    scrollId string
}


// Do returns the next search result. It will return io.EOF as error if there
// are no more search results.
func (s *ScrollService) Do(ctx context.Context) (*SearchResult, error) {
    s.mu.RLock()
    nextScrollId := s.scrollId
    s.mu.RUnlock()
    if len(nextScrollId) == 0 {
        return s.first(ctx)
    }
    return s.next(ctx)
}

このstruct自体に問題があるわけではない。作ったstructのDo()というメソッドを呼ぶことでElasticSearchにrequestを投げることができる。これ自体はミドルウェアへのアクセスを提供するライブラリとして一般的な定義ではあると思う。

一方で、structは設定として、pagination時のsizeを指定できたり、やscrollId(cursor)を既に保持している場合に渡せたりということができる。内部的にはunexported fieldなのだけれど。とりあえず色々な設定のためのAPIを持っている。

// Size specifies the number of documents Elasticsearch should return
// from each shard, per page.
func (s *ScrollService) Size(size int) *ScrollService {
    s.size = &size
    return s
}

// ScrollId specifies the identifier of a scroll in action.
func (s *ScrollService) ScrollId(scrollId string) *ScrollService {
    s.mu.Lock()
    s.scrollId = scrollId
    s.mu.Unlock()
    return s
}

Size()は自分自身のsizeを変更し、自分自身を返す。ScrollId()も同様。

実はこれらのAPIが曲者なのでは?というのがこの記事の主題。あるstructの状態(設定)を変更するためのAPIとして自分自身を返すAPIの定義が良いのかどうかという話。

どういう時に嫌になる?

普通に使う分には問題ない。普通に使うというのは、提供されているAPIを直接使う場合のこと。

response, err := scroll.Size(30).Do(ctx)

では普通ではない使いかたとはどういうことかというとインターフェイスを切ろうとする場合。定義されている型を直接使うのではなく、自分で定義したような何らかの型から間接的に扱いたいような場合。

テストを書こうとインターフェイスに切り出そうとする場合

例えばちょっとしたテストを書こうと外部リソースへのアクセスをインターフェイスに切り出すということはよくあることだと思う。例えば以下の記事ではHTTPアクセスに対して自分自身でinterfaceを定義して使う例。

雑にまとめると以下のようなインターフェイスを定義してあげるとテストとか書きやすくて楽だよね。という話。

type HttpClient interface {
    Get(string) (*http.Response, error)
}

さて、これを先程の自分自身を返すメソッドを持つstructに適用しようとしてみよとすると...できない。定義しづらい。設定の機能を持たない単純なinterfaceは以下の様になる。

// Doしか持たないinterfaceをScrollServiceという風に名前をつけるのはおかしいけれど

type ScrollService interface {
    Do(ctx context.Context) (*elastic.SearchResult, error)
}

ここで先程の設定を行うAPIに関する部分もコードで利用してしまっているとする(本来はもう少し複雑なもの。あるいは自分で定義したstructのメソッド)。

func Fetch(ctx context.Context, scroll *elastic.ScrollService, size int) ([]Document, error) {
    response, err := scroll.Size(size).Do(ctx)
    if err != nil {
        return nil, err
    }

    var docs []Document

    // responseを良い感じにparseして[]Documentを得る
    ..

    return docs, nil
}

このとき先程のinterfaceではコンパイルが通らない。Size() も使っているので。

それでは自分自身を返すメソッドも要求してみることにしよう。

type ScrollService interface {
    Do(ctx context.Context) (*elastic.SearchResult, error)
    Size(size int) ScrollService
}

このようなインターフェイスで先程のFetch()関数のコンパイルは通るようになる。しかし、大本の*elastic.ScrollServiceはこのinterfaceを満足しない。以下2つは異なるので。

func Size(int) ScrollService
func Size(int) *elastic.ScrollService

これが問題。goには自分自身の型を表現する型などというものなどは存在していないので。interfaceを切るときにどうしても元の実装への依存を要求してしまう。もし仮に自分自身を返すのを止めているのならばinterfaceを定義することができた(SetSize()という名前の方が妥当かもしれない)。

type ScrollService interface {
    Do(ctx context.Context) (*elastic.SearchResult, error)
    Size(size int)
}

もちろん元のコードの scroll.Size(size).Do(ctx) の部分は2行にわけられて書かれるようになる。

と、まぁ、そんなわけで自分自身を返すAPIというのは良い案ではないのではないかと感じている。なんというか、利便性を求めて生やしたAPIが、ちょっと凝った使いかたをしようとしたときに(e.g. テストのときに)重荷になってしまっていると表現しても良いような状態になってしまっている感じ。

問題への対応

問題への対応もワークアラウンド的なものは考えられなくはない。

wrapしたstructを定義

1つは元のstructをwrapしたstructを定義すること。このようにしてあげれば自分で定義した戻り値無しのinterfaceを満足する実装になる。

type myScrollService struct {
    *elastic.ScrollService
}

func (s *myScrollService) Size(size int) {
    s.ScrollService.Size(size)
}

しかし、正直な所、このようなwrapperを毎回書くのは馬鹿馬鹿しい。

利用する部分で要求するinterfaceを小さくする

あるいは利用する部分で要求するinterfaceを小さくすることもできるかもしれない。これは先程のFetch関数を以下のように2つの関数に分離するということ。

func Fetch(ctx context.Context, scroll *elastic.ScrollService, size int) ([]Document, error) {
    return fetch(ctx, scroll.Size(size))
}

func fetch(ctx context.Context, scroll ScrollService) ([]Document, error) {
    response, err := scroll.Do(ctx)
    if err != nil {
        return nil, err
    }

    var docs []Document

    // responseを良い感じにparseして[]Documentを得る
    ..

    return docs, nil
}

そしてテストのときには Do() だけを要求するinterfaceとしてfetch()に対してテストを書く。これもこれでテストのために面倒な実装を要求するという点で微妙な感じ(とは言え、元の実装を変えないという意味では一番マシなような気もしている)。

元の実装のへの依存を要求することを我慢する

あとは、元の実装のへの依存を要求することを我慢する。諦めてインターフェイスを無理やり定義してしまう方法。一番実装コードの変更が少ないものの。こちらはコンパイルは通るもののテストでエラーみたいな形になって利用するときの精神的コストが幾分か高めになりつらそうな気がしている。

このときのインターフェイスは以下のようなもの。

type ScrollService interface {
    Do(ctx context.Context) (*elastic.SearchResult, error)
    Size(size int) *elastic.ScrollService
}

gomvpkg-lightというパッケージを作った

github.com

作りました。

発端

まとめると

  • もう少し速さが欲しい
  • packageを再帰的に移動してほしくない場合がある

成果

速さ

gcを切るみたいなズルも含めて2~3倍位早いと言うような感じにはなった。ちなみに対象としたソースコードは某社の某アプリケーション(ソースコードは非公開)。変換の過程で読み込まれたパッケージは標準ライブラリも全部込みで600近く(599)。

gomvpkg

$ time gomvpkg -from github.com/xxx/myapp/model -to github.com/xxx/myapp/model2
...

real    0m14.696s
user    0m33.366s
sys 0m8.601s

gomvpkg-light

$ gomvpkg-light --in github.com/xxx/myapp --from github.com/xxx/myapp/model --to github.com/xxx/myapp/model2 --disable-gc
2018/04/08 23:22:43 start move package github.com/xxx/myapp/model -> github.com/xxx/myapp/model2
2018/04/08 23:22:43 get in-pkg /home/nao/go/src/github.com/xxx/myapp
2018/04/08 23:22:43 collect candidate directories 683
2018/04/08 23:22:43 collect affected packages 132
2018/04/08 23:22:43 loading packages..
2018/04/08 23:22:47 599 packages are loaded
...
2018/04/08 23:22:48 move package github.com/xxx/myapp/model -> github.com/xxx/myapp/model2
2018/04/08 23:22:48 takes 4.952462621s
2018/04/08 23:22:48 end

real    0m5.283s
user    0m11.218s
sys 0m3.536s

小さめなコードの場合

ちなみにおそらくだけれど。gomvpkgのGOPATH以下を(ほとんど)全部走査するという固定費のようなコストが削減されるので。小さめのアプリケーションでの速度改善はもう少し大きいかもしれない。

例えば自分の手元のてきとうなパッケージを変更してみると

gomvpkg

$ gomvpkg -from github.com/podhmo/handwriting/generator/deriving -to github.com/podhmo/handwriting/deriving2
Renamed 2 occurrences in 1 file in 1 package.

real    0m6.131s
user    0m14.269s
sys     0m6.464s

gomvpkg-light

$ gomvpkg-light --from github.com/podhmo/handwriting/generator/deriving --to github.com/podhmo/handwriting/deriving2 --in github.com/podhmo/handwriting
...

real    0m1.263s
user    0m3.223s
sys     0m0.463s

(ちなみに --disable-gc をつけると1秒切る程度になる)

packageを再帰的に移動してほしくない場合がある

packageを再帰的に移動してほしくない場合がある。というのはわかりやすく言えばサブパッケージを移動してほしくないということ。たとえば以下みたいなパッケージがあったとして。

github.com/podhmo/xxx/foo
github.com/podhmo/xxx/foo/bar
github.com/podhmo/xxx/foo/boo

fooをbazにmvpkgする時に以下のようになって欲しいことがあるということ。

github.com/podhmo/xxx/baz
github.com/podhmo/xxx/foo/bar
github.com/podhmo/xxx/foo/boo

このために --only オプションを作った。以下のようにすると、fooだけを移動する。

$ gomvpkg-light --only --in github.com/podhmo/xxx --from github.com/podhmo/xxx/foo --to github.com/podhmo/xxx/baz