読者です 読者をやめる 読者になる 読者になる

swagger-marshmallow-codegen でpaths以下も見るようにした

python swagger marshmallow

swagger-marshmallow-codegen でpaths以下も見るようにした。あまりきれいとはいえない感じかもしれないけれど。

paths以下を見るということ

今まではdefinitions以下しか見なかったのだけれど。通常swaggerでapiの定義をするときにはpaths以下にも色々書く。というよりrequestとresponseがどのようになっているかはpaths以下の定義を見る事が多い。例えば以下のようなAPI定義(apiaryのtuotialからもらってきた)はどうなっているかというと。

paths:
  /message/{name}:
    x-summary: Message operations
    x-description: Operation description in Markdown
    get:
      summary: Get a message of the day
      description: |
       Description of the operation in Markdown
      operationId: getMessage
      parameters:
        - name: name
          in: path
          description: name to include in the message
          type: string
          x-example: 'Hello, Adam!'
      responses:
        default:
          description: Bad request
        200:
          description: Successful response
          schema:
            $ref: '#/definitions/Message'
          examples:
            'application/json':
              message: 'Hello, Adam!'
definitions:
  Message:
    required:
      - message
    properties:
      message:
        type: string
        default: 'Hello, Adam!'

これは GET /message/{name} というようなAPIが存在していて、そのrequestの形式で許可するものはpathのみ(apiaryのsampleはqueryになっていたけれどそれは間違い。こちらでは修正している)。また、outputとしてstatus=200のresponseは Message のschemaになっている。

Input, Output

先程のAPI定義からswagger-marshmallo-codegenを使ってschemaのコードを生成してみる。今度からInput,Outputというclassも生成されるようになった。具体的には以下の様なもの。

# -*- coding:utf-8 -*-
from marshmallow import (
    Schema,
    fields
)


class Message(Schema):
    message = fields.String(required=True, missing=lambda: 'Hello, Adam!')


class MessageNameInput(object):
    class Get(object):
        """Get a message of the day"""

        class Path(Schema):
            name = fields.String(description='name to include in the message')


class MessageNameOutput(object):
    class Get200(Message):
        """Successful response"""
        pass

APIのrequestとresponse毎にInput,Outputが存在していてそのバリエーション毎にクラスが別れている。外側のクラスはnamespaceみたいなもの。

今回のAPIに関して言えば、以下のようなrequestとresponseになる。

GET /message/adam
{"message": "Hello, Adam!"}

それぞれ MessageNameInputMessageNameOutput が対応している。

x-marshmallow-name

Input,Outputの名前はpathのpatternからすごく雑に変換して決めている。

/message/{name} -> /message/name -> message, name -> MessageName

気に入らない場合もあるだろうから、 x-marshmallow-name で名前を決められるようにした。

--- 00schema.yaml    2017-01-17 06:29:00.000000000 +0900
+++ 01schema.yaml 2017-01-17 06:33:33.000000000 +0900
@@ -2,6 +2,7 @@
   /message/{name}:
     x-summary: Message operations
     x-description: Operation description in Markdown
+    x-marshmallow-name: Message
     get:
       summary: Get a message of the day
       description: |

以下のような修正を加えると出力結果は以下の様に変わる。

# -*- coding:utf-8 -*-
from marshmallow import (
    Schema,
    fields
)


class Message(Schema):
    message = fields.String(required=True, missing=lambda: 'Hello, Adam!')


class MessageInput(object):
    class Get(object):
        """Get a message of the day"""

        class Path(Schema):
            name = fields.String(description='name to include in the message')


class MessageOutput(object):
    class Get200(Message):
        """Successful response"""
        pass

もう少し複雑なもの

Path以外にもparametersは色々ある。query,formData,body,header (詳しくはここ)。それらも見る。

例えばGithubAPIの一部のAPI定義をすこしだけ弄った以下のようなyamを渡すと以下のようなコードを生成する。

definitions:
  emailsPost:
    items:
      type: string
      pattern: ".+@.+"
    type: array
  label:
    properties:
      color:
        maxLength: 6
        minLength: 6
        type: string
      name:
        type: string
      url:
        type: string
    type: object
  labels:
    items:
      $ref: '#/definitions/label'
    type: array
  labelsBody:
    items:
      type: string
    type: array

parameters:
  owner:
    description: Name of repository owner.
    in: path
    name: owner
    required: true
    type: string
  repo:
    description: Name of repository.
    in: path
    name: repo
    required: true
    type: string
  number:
    description: Number of issue.
    in: path
    name: number
    required: true
    type: integer
  X-Github-Media-Type:
    description: |
      You can check the current version of media type in responses.
    in: header
    name: X-GitHub-Media-Type
    type: string
  Accept:
    description: Is used to set specified media type.
    in: header
    name: Accept
    type: string
  X-RateLimit-Limit:
    in: header
    name: X-RateLimit-Limit
    type: integer
  X-RateLimit-Remaining:
    in: header
    name: X-RateLimit-Remaining
    type: integer
  X-RateLimit-Reset:
    in: header
    name: X-RateLimit-Reset
    type: integer
  X-GitHub-Request-Id:
    in: header
    name: X-GitHub-Request-Id
    type: integer


responses:
  labels:
    description: OK
    schema:
      $ref: '#/definitions/labels'
  label-created:
    description: Created
    schema:
      $ref: '#/definitions/label'


paths:
  '/repos/{owner}/{repo}/issues/{number}/labels':
    delete:
      description: Remove all labels from an issue.
      parameters:
        - $ref: "#/parameters/owner"
        - $ref: "#/parameters/repo"
        - $ref: "#/parameters/number"
        - $ref: "#/parameters/X-Github-Media-Type"
        - $ref: "#/parameters/Accept"
        - $ref: "#/parameters/X-RateLimit-Limit"
        - $ref: "#/parameters/X-RateLimit-Remaining"
        - $ref: "#/parameters/X-RateLimit-Reset"
        - $ref: "#/parameters/X-GitHub-Request-Id"
      responses:
        '204':
          description: |
            No content.
        '403':
          description: |
            API rate limit exceeded. See http://developer.github.com/v3/#rate-limiting
            for details.
    get:
      description: List labels on an issue.
      parameters:
        - $ref: "#/parameters/owner"
        - $ref: "#/parameters/repo"
        - $ref: "#/parameters/number"
        - $ref: "#/parameters/X-Github-Media-Type"
        - $ref: "#/parameters/Accept"
        - $ref: "#/parameters/X-RateLimit-Limit"
        - $ref: "#/parameters/X-RateLimit-Remaining"
        - $ref: "#/parameters/X-RateLimit-Reset"
        - $ref: "#/parameters/X-GitHub-Request-Id"
      responses:
        '200':
          $ref: "#/responses/labels"
        '403':
          description: |
            API rate limit exceeded. See http://developer.github.com/v3/#rate-limiting
            for details.
    x-marshmallow-name: IssuedLabels
    post:
      description: Add labels to an issue.
      parameters:
        - $ref: "#/parameters/owner"
        - $ref: "#/parameters/repo"
        - $ref: "#/parameters/number"
        - $ref: "#/parameters/X-Github-Media-Type"
        - $ref: "#/parameters/Accept"
        - $ref: "#/parameters/X-RateLimit-Limit"
        - $ref: "#/parameters/X-RateLimit-Remaining"
        - $ref: "#/parameters/X-RateLimit-Reset"
        - $ref: "#/parameters/X-GitHub-Request-Id"
        - in: body
          name: body
          required: true
          schema:
            $ref: '#/definitions/emailsPost'
      responses:
        '201':
          $ref: "#/responses/label-created"
        '403':
          description: |
            API rate limit exceeded. See http://developer.github.com/v3/#rate-limiting
            for details.
    put:
      description: |
        Replace all labels for an issue.
        Sending an empty array ([]) will remove all Labels from the Issue.
      parameters:
        - $ref: "#/parameters/owner"
        - $ref: "#/parameters/repo"
        - $ref: "#/parameters/number"
        - $ref: "#/parameters/X-Github-Media-Type"
        - $ref: "#/parameters/Accept"
        - $ref: "#/parameters/X-RateLimit-Limit"
        - $ref: "#/parameters/X-RateLimit-Remaining"
        - $ref: "#/parameters/X-RateLimit-Reset"
        - $ref: "#/parameters/X-GitHub-Request-Id"
        - in: body
          name: body
          required: true
          schema:
            $ref: '#/definitions/emailsPost'
      responses:
        '201':
          $ref: "#/responses/label-created"
        '403':
          description: |
            API rate limit exceeded. See http://developer.github.com/v3/#rate-limiting
            for details.

こういう感じ。

# -*- coding:utf-8 -*-
from marshmallow import (
    Schema,
    fields
)
from marshmallow.validate import (
    Length,
    Regexp
)
from swagger_marshmallow_codegen.schema import (
    PrimitiveValueSchema
)
import re


class Label(Schema):
    color = fields.String(validate=[Length(min=6, max=6, equal=None)])
    name = fields.String()
    url = fields.String()


class IssuedLabelsInput(object):
    class Delete(object):
        class Header(Schema):
            X_GitHub_Media_Type = fields.String(description='You can check the current version of media type in responses.\n', dump_to='X-GitHub-Media-Type', load_from='X-GitHub-Media-Type')
            Accept = fields.String(description='Is used to set specified media type.')
            X_RateLimit_Limit = fields.Integer(dump_to='X-RateLimit-Limit', load_from='X-RateLimit-Limit')
            X_RateLimit_Remaining = fields.Integer(dump_to='X-RateLimit-Remaining', load_from='X-RateLimit-Remaining')
            X_RateLimit_Reset = fields.Integer(dump_to='X-RateLimit-Reset', load_from='X-RateLimit-Reset')
            X_GitHub_Request_Id = fields.Integer(dump_to='X-GitHub-Request-Id', load_from='X-GitHub-Request-Id')

        class Path(Schema):
            owner = fields.String(description='Name of repository owner.')
            repo = fields.String(description='Name of repository.')
            number = fields.Integer(description='Number of issue.')


    class Get(object):
        class Header(Schema):
            X_GitHub_Media_Type = fields.String(description='You can check the current version of media type in responses.\n', dump_to='X-GitHub-Media-Type', load_from='X-GitHub-Media-Type')
            Accept = fields.String(description='Is used to set specified media type.')
            X_RateLimit_Limit = fields.Integer(dump_to='X-RateLimit-Limit', load_from='X-RateLimit-Limit')
            X_RateLimit_Remaining = fields.Integer(dump_to='X-RateLimit-Remaining', load_from='X-RateLimit-Remaining')
            X_RateLimit_Reset = fields.Integer(dump_to='X-RateLimit-Reset', load_from='X-RateLimit-Reset')
            X_GitHub_Request_Id = fields.Integer(dump_to='X-GitHub-Request-Id', load_from='X-GitHub-Request-Id')

        class Path(Schema):
            owner = fields.String(description='Name of repository owner.')
            repo = fields.String(description='Name of repository.')
            number = fields.Integer(description='Number of issue.')


    class Post(object):
        class Body(PrimitiveValueSchema):
            v = fields.String(validate=[Regexp(regex=re.compile('.+@.+'))])

        class Header(Schema):
            X_GitHub_Media_Type = fields.String(description='You can check the current version of media type in responses.\n', dump_to='X-GitHub-Media-Type', load_from='X-GitHub-Media-Type')
            Accept = fields.String(description='Is used to set specified media type.')
            X_RateLimit_Limit = fields.Integer(dump_to='X-RateLimit-Limit', load_from='X-RateLimit-Limit')
            X_RateLimit_Remaining = fields.Integer(dump_to='X-RateLimit-Remaining', load_from='X-RateLimit-Remaining')
            X_RateLimit_Reset = fields.Integer(dump_to='X-RateLimit-Reset', load_from='X-RateLimit-Reset')
            X_GitHub_Request_Id = fields.Integer(dump_to='X-GitHub-Request-Id', load_from='X-GitHub-Request-Id')

        class Path(Schema):
            owner = fields.String(description='Name of repository owner.')
            repo = fields.String(description='Name of repository.')
            number = fields.Integer(description='Number of issue.')


    class Put(object):
        class Body(PrimitiveValueSchema):
            v = fields.String(validate=[Regexp(regex=re.compile('.+@.+'))])

        class Header(Schema):
            X_GitHub_Media_Type = fields.String(description='You can check the current version of media type in responses.\n', dump_to='X-GitHub-Media-Type', load_from='X-GitHub-Media-Type')
            Accept = fields.String(description='Is used to set specified media type.')
            X_RateLimit_Limit = fields.Integer(dump_to='X-RateLimit-Limit', load_from='X-RateLimit-Limit')
            X_RateLimit_Remaining = fields.Integer(dump_to='X-RateLimit-Remaining', load_from='X-RateLimit-Remaining')
            X_RateLimit_Reset = fields.Integer(dump_to='X-RateLimit-Reset', load_from='X-RateLimit-Reset')
            X_GitHub_Request_Id = fields.Integer(dump_to='X-GitHub-Request-Id', load_from='X-GitHub-Request-Id')

        class Path(Schema):
            owner = fields.String(description='Name of repository owner.')
            repo = fields.String(description='Name of repository.')
            number = fields.Integer(description='Number of issue.')




class IssuedLabelsOutput(object):
    class Get200(Label):
        """OK"""
        def __init__(self, *args, **kwargs):
            kwargs['many'] = True
            super().__init__(*args, **kwargs)


    class Post201(Label):
        """Created"""
        pass

    class Put201(Label):
        """Created"""
        pass

signal handleするコードのテスト

python

はじめに

signalをhandleするコード自体は手軽に書ける。

import signal
import sys


def on_sigint(signum, frame):
    print("hmm")
    sys.exit(1)

signal.signal(signal.SIGINT, on_sigint)

しかしこれが確実にtrapされたことを確認するテストを書くのはだるい

面倒くさい理由

面倒くさい理由はいくつかあって、まず、signalをtrapするというのはプログラム全体に影響を及ぼす。 そしてmain threadでしか受け取れないので気軽にthreadingでごまかすということも出来ない。

試行錯誤した結果

しょうがないのでmultiprocessingで頑張る。

import sys
import unittest
import signal


class Ob(object):
    def __str__(self):
        return hex(id(self))


def do_something(calculate, _on_trap=None):
    ob = Ob()

    def on_trap(signum, frame):
        if _on_trap is not None:  # for test
            _on_trap(ob)
        print("cleanup with ", ob)
        sys.exit(1)

    signal.signal(signal.SIGHUP, on_trap)
    signal.signal(signal.SIGINT, on_trap)
    signal.signal(signal.SIGTERM, on_trap)

    # fetch anything?

    calculate(ob)  # do something

    # save db?


class Tests(unittest.TestCase):
    def test_it(self):
        from multiprocessing import Process, Queue
        import time

        q = Queue()
        init = 1
        called = 10
        q.put(init)

        def calculate(ob):
            print("before calculate", ob)
            time.sleep(1)  # waiting for killed
            print("after calculate", ob)

        def _on_trap(ob):
            self.assertEqual(q.get(), init)
            q.put(called)

        p = Process(target=lambda: do_something(calculate, _on_trap=_on_trap))
        p.start()
        time.sleep(0.1)
        p.terminate()  # SIGTERM
        p.join()
        self.assertEqual(q.get(), called)

if __name__ == "__main__":
    unittest.main()
    # before calculate 0x1030b66d8
    # cleanup with  0x1030b66d8

afterは呼ばれていないので途中で中断されている(process.terminate()によるSIGTERM)。 そしてqueueの値はcalledになっている。

before calculate 0x103f6f7b8
cleanup with  0x103f6f7b8
.
----------------------------------------------------------------------
Ran 1 test in 0.182s

OK

swagger-marshmallow-codegenでカスタマイズ出来るようにした

python swagger marshmallow

swagger-marshmallow-codegenで簡単なカスタマイズ出来るようにした。

例えば以下の様なことができるようになった

  • defaultで使うschema classをMySchemaに変える
  • 特定の条件を満たした値のときには自分で作った独自のfieldを使うように変える

ただこれらはすごくwork-aroundっぽい方針で作っているのであんまり綺麗ではないかもしれない。自分でDriverというクラスを作りそのクラスを --driver に渡す感じで使う。例えば以下の様な形。

$ swagger-marshmallow-codegen --driver=_custom.py:MyDriver --logging=DEBUG person.yaml > person.py

defaultで使うschema classをMySchemaに変える

defaultで使うschema classを変えるには codegen_factory を変える。myschema モジュールのMySchemaが使いたい場合には以下の様にする。

from swagger_marshmallow_codegen.driver import Driver

class MyDriver(Driver):
    codegen_factory = Driver.codegen_factory.override(schema_class_path="myschema:MySchema")

特定の条件を満たした値のときには自分で作った独自のfieldを使うように変える

こちらも同様に dispatcher_factory を変える。

例えば format=objectId のものは自分で定義した myschema の ObjectIdを使うように変えるときには以下の様にする。default値を気にせずmappingを変更する場合には、以下だけで良い。

type_map = {
    Pair(type="string", format="objectId"): "myschema:ObjectId",
    **TYPE_MAP,
}


class MyDriver(Driver):
    codegen_factory = Driver.codegen_factory.override(schema_class_path="myschema:MySchema")
    dispatcher_factory = Driver.dispatcher_factory.override(type_map=type_map)

とは言えdefault値の扱いを考えるとこちらは少し頑張らないとだめ。

from swagger_marshmallow_codegen.driver import Driver
from swagger_marshmallow_codegen.dispatcher import TYPE_MAP, Pair, FormatDispatcher, ReprWrapString


class MyDispatcher(FormatDispatcher):
    type_map = {
        Pair(type="string", format="objectId"): "myschema:ObjectId",
        **TYPE_MAP,
    }

    def dispatch_default(self, c, value, field):
        if isinstance(value, bson.ObjectId) or field.get("format") == "objectId":
            c.import_("bson")
            return ReprWrapString("bson.{!r}".format(bson.ObjectId(value)))
        return super().dispatch_default(c, value, field)


class MyDriver(Driver):
    codegen_factory = Driver.codegen_factory.override(schema_class_path="myschema:MySchema")
    dispatcher_factory = MyDispatcher

実行結果

例えば上で定義したものを使うと。以下のようなyaml

definitions:
  person:
    type: object
    properties:
      id:
        type: string
        format: objectId
        default: 5872bad4c54d2d4e78b34c9d
      name:
        type: string
      age:
        type: integer
    required:
      - name

このようなpythonのコードになる。

# -*- coding:utf-8 -*-
from myschema import (
    MySchema,
    ObjectId
)
from marshmallow import fields
import bson


class Person(MySchema):
    id = ObjectId(missing=lambda: bson.ObjectId('5872bad4c54d2d4e78b34c9d'))
    name = fields.String(required=True)
    age = fields.Integer()

参考

一応、参考にするための example も作った。

補足

ちなみにmyschemaのコードは例えば以下のようなもの

import bson
from marshmallow import Schema, fields


class MySchema(Schema):
    class Meta:
        ordered = True
        strict = True


class ObjectId(fields.String):
    default_error_messages = {
        'invalid_object_id': 'Not a valid bson.ObjectId.',
    }

    def _validated(self, value):
        """Format the value or raise a :exc:`ValidationError` if an error occurs."""
        if value is None:
            return None
        if isinstance(value, bson.ObjectId):
            return value
        try:
            return bson.ObjectId(value)
        except (ValueError, AttributeError):
            self.fail('invalid_object_id')

    def _deserialize(self, value, attr, data):
        return self._validated(value)

    def _serialize(self, value, attr, data):
        if not value:
            return value
        return str(value)

こういうちょっとしたデータの受け渡しどうするんだという話

python memo dictknife

はじめに

今自分で作っている dictknife というリポジトリについにコマンドを追加してしまった。 色々あるのだけれど。今回は dictknife transform の話。

transform

何かしらの形状の変換をしたいことがある。

例えば、こういう入力を受け取って、

properties:
  name:
    type: string
    description: name of something
  age:
    type: integer
    minimum: 0

こういう出力を返したい。

definitions:
  person:
    properties:
      name:
        type: string
        description: name of something
      age:
        type: integer
        minimum: 0

結局、load,dumpを無視するとコード自体は以下だけなのだけれど。

def transform(d):
    return {"definisions": {"person": d}}

これを省力な形で提供するのがちょっとだけ面倒。

面倒くさい点

面倒くさい点は2つある

  • transform 関数の取得
  • transform 関数へ引数を渡したい場合の方法

transform 関数の取得

上の方法で考えた変換(definisions.personでwrapするもの)がもし仮にどこかのpackageで提供されているとする。 すると以下の様に書ける気がする。

package pathを指定する場合

例えば、 foo.bar.transform:lifting で提供されている場合は以下の様に書ける。

$ dictknife transform --function foo.bar.transform:lifting ...

でも、これはちょっと使いづらい。そもそも何度も使って便利だと分かっているものでなければpackageになっていることが少ない。テキトウにファイルを置いてPYTHONPATHを追加するという方法でできなくもないけれど。やっぱり面倒。

$ PYTHONPATH=../myscript dictknife transform --function transform:lifinting ...

package path or 物理的な pathで指定する場合

直接ファイルを指定出来るようになれば十分か?一応、昔作ったmagicalimport というpackageを使うとそれは出来る。例えば上の例は以下のように書けるように出来る。

$ dictknife transform --function ../myscript/transform.py:lifting ...

eval的な何か

しかし、それでも使いにくい。何かしらのちょっとした処理を行いたいときには、一時的なファイルすら作りたくない場合がある。(というよりも、temporaryなscriptや関数群の置き場を決めるという意思決定がしたくないというような状況)。仕方が無いので禁断の果実であるevalを使うことにする。

$ dictknife transform --code 'lambda d: {"definitions": {"person": d}}' ...

transform 関数へ引数を渡したい場合の方法

trasnform 関数が取得できれば万事OKという訳でもない。冒頭の変換について考えてみても、常に "person" という固定の名前で変換したいという状況はあんまりない。どうにかしてtransform 関数へ情報を受け渡したい(そもそもtransformは関数だけで十分なのかという話もあるけれど。あんまり複雑なことを考えたくはないので今回は関数で良いということにしてみる)。

コマンドライン引数で渡す方法

コマンドライン引数で渡す方法はすごく分かりやすい。個別にコマンドを作るということを念頭に置くならこの形が最適かもしれない。とは言え、これを汎用的に提供する機能を作ろうとすると、もはやtransform コマンドのジェネレーターのようなものを作る事になってしまう。

$ dictknife transform --name person --code 'lambda d, name="NAME": {"definisions": {name: d}}' ...

configファイルで渡す方法

汎用的なtransformということを考えると以下の様な関数を作る事にならざる負えない。

def lifting(data, **kwargs):
    ...

pythonに限って言えばキーワード引数になっている方がべんりかもしれない。

def lifting(data, name="NAME"):
    ...

幸い functools.partial に辞書を渡してあげるとキーワード引数を埋める事ができる。

from functools import partial


fn = partial(lifting, **{"name": "person"})
fn(data)  # transform!!

dictを受け取って**で展開してあげれば良いかもしれない。dictを取得する方法を考えてみる。 幸い元々JSONYAMLを入出力するライブラリ上のコマンドなのでconfig用の情報をこれらのフォーマットで受け取るという形で考えても良いかもしれない。

$ dictknife transform --function "./myscript.py:lifting" --config-file ./config.json ...

とは言え、これは transform 関数を作ったときと同じ状況に陥る。本当に単純な処理に関してはファイルなんて作りたくない。

JSONで受け取れる引数を追加する

基本的にはシェル上のコマンドとJSONを直接扱う方法と言うのはあまり良い方法とは思えないのだけれど。jo やその類型のものを使えば幾分かマシになるだろうということで。JOSNを直接受け取れるようにする。

$ dictknife transform --code 'lambda d,name="foo": {"definisions": {name: d}}' --config '{"name": "person"}' ...

一応ワンライナーで済ませる事が出来るようになった。

その他細々としたこと

パイプで繋げられるようにしたい。パイプで繋げられるようなインターフェイスと言うのは以下のようなもの

$ cat src.yaml | dictknife transform <> > dst.yaml 

とは言え、明示的に入出力を指定したい場合もある(go generateで使うときなど)。

$ dictknife transform --src src.yaml --dst dst.yaml <>

現在の状態

現在の状態は以下のようなもの。真面目にdescriptionは書いていないですね。。

$ dictknife transform
Usage: dictknife transform [OPTIONS]

  transform dict

Options:
  --src PATH
  --dst PATH
  --config TEXT
  --config-file PATH
  --code TEXT
  --function TEXT
  --help              Show this message and exit.
  • --src 入力ファイル
  • --dst 出力ファイル
  • --config transform関数に渡すdictのリテラル的な文字列を受け取る
  • --config-file transform関数に渡すdictのファイル(configのファイルversion)
  • --code transform関数のワンラインナーを書きたいときに使う(eval)
  • --function transform関数

追記:

環境変数で設定するみたいな方法もあるのかもしれない?とは言えネストした構造がつらそう。

追記:

JSONを文字列で受け取れるような構造は jq で取り出す形にすると相性が良いかもしれない。

pyramid-swagger-routerというパッケージを作りました

python pyramid swagger

pyramid-swagger-router というパッケージを作りました。swaggerの定義ファイル(swagger.yaml)からpyramidのviewの定義のコードを生成するパッケージです。

特徴

特徴は、pyramid-swagger-routerという名前でありながら router に値するものが何もないということです。routingのための定義を自動生成する感じのツールです。 標語の様な形にするなら以下の様な感じです。

Code generation is better than meta-programming, and onetime scaffold is simply bad.

code generation is better than meta-programming

コード生成がメタプログラミングより良いと言うのは以下の点です。

そうです。止めたいときにはただただ使うのを止めれば良いのです。何にも依存していないし。何の汚染もしません。素晴らしい。

onetime scaffold is simply bad

後の句の一度きりのscaffoldは単純に悪いというのはどういうことでしょうか?

これは、変更の時のことを考えてみてください。ある設定ファイル(e.g. swagger.yaml)から何らかのコードを生成したいとします(e.g. views.py)。 この時に、ありがちなscaffoldの機能では、単にコードを書く上での雛形となるようなskeleton的なコードを生成します。もちろん最初に作るタイミングでは便利なものではあるのですが。ここで生成されたコードに変更が加えられた時のことなどを考えてみてください(scaffoldで生成されたコードの結果が変わったということ(version X -> version Y))。

この時例えば以下の様な状態になっていると思います。

- foo/views.py  # version Xの段階でscaffold
- bar/views.py  # version Yの段階でscaffold

この時 bar/views.py で使われている生成結果のコードに追従するために foo/views.py の変更が必要になります。ここで単純なscaffoldの機能は無力な存在になります。人間がコードの変更を行わなくてはいけません。かなしい。

一方で、今回作った pyramid-swagger-router に関してはこのようなことが発生しません。発生しないように注意深く作ったので。具体的には、コード生成により変更する箇所を特定の部分に絞るということと、古いコードに対して変更適用をする際に一度元のコードのFST(full syntax tree)を取り出してからscaffoldにより生成されるであろうコードでmergeするみたいなことをしています。具体例をあげなければ分かりづらいかと思いますが。すごく雑に言うなら、修正を反映したかったらもう一度何度でもコマンドを実行すれば良いということです。

使い方

普通にインストールします。

$ pip install pyramid-swagger-router  # まだできないあとで出来るようにする

基本的な使い方は以下だけです。swaggerの定義ファイルを渡してあげると良い感じにコードを生成してくれます。簡単ですね。

$ pyramid-swagger-router <swagger.yaml> <dst>

connexion-example

swagger定義ファイルを見てあれこれやってくれるflask上のフレームワークconnexion というものもあったりします。これはメタプログラミングベースのパッケージです。これ用のexampleを公開してくれている人がいたのでこのexampleを使ったコードを書いてみようと思います。

以下のようなswagger.yamlを使います。

さすがに全部引用すると長すぎるので、定義されているAPIだけを以下に書いておきます。

GET    /pets
GET    /pets/{pet_id}
PUT    /pets/{pet_id}
DELETE /pets/{pet_id}

pet という以下の様な構造のオブジェクトに対するsimpleなCRUDAPIです。

definitions:
  Pet:
    type: object
    required:
      - name
      - animal_type
    properties:
      id:
        type: string
        description: Unique identifier
        example: "123"
        readOnly: true
      name:
        type: string
        description: Pet's name
        example: "Susie"
        minLength: 1
        maxLength: 100
      animal_type:
        type: string
        description: Kind of animal
        example: "cat"
        minLength: 1
      tags:
        type: object
        description: Custom tags
      created:
        type: string
        format: date-time
        description: Creation time
        example: "2015-07-07T15:49:51.230+02:00"
        readOnly: true

exampleの実装

scaffold

exampleと似たようの挙動をするものを実装してみます。色々な都合上1つのファイルでは無理なので幾つかのファイル分けて実装します。

$ mkdir app
$ wget https://raw.githubusercontent.com/hjacobs/connexion-example/master/swagger.yaml
$ gsed -i 's@app.@app.views.@' swagger.yaml
$ pyramid-swagger-router swagger.yaml
 INFO:                    prestring.output:touch directory path=./app
 INFO:                    prestring.output:touch file path=./app/routes.py
 INFO:                    prestring.output:touch file path=./app/views.py

以下の様な形で初回のscaffoldはOK。views.pyに定義を書いていきます。

app/views.pyには以下のような形のscaffoldにより生成されたコードが出力されてます。

from pyramid.view import(
    view_config
)


@view_config(renderer='json', request_method='GET', route_name='app_views')
def get_pets(context, request):
    """
    Get all pets

    request.GET:

        * 'animal_type'  -  `{"type": "string", "pattern": "^[a-zA-Z0-9]*$"}`
        * 'limit'  -  `{"type": "integer", "minimum": 0, "default": 100}`
    """
    return {}

同様にapp/routes.pyにも以下のようなコードが生成されます。

def includeme_swagger_router(config):
    config.add_route('app_views', '/pets')
    config.add_route('app_views1', '/pets/{pet_id}')
    config.scan('.views')


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

viewの実装

ここは特に何か特殊なことをしません。とりあえず元のexampleのコードを適宜コピーして実装してしまいます。

diff --git a/app/views.py b/app/views.py
index 789df11..1cc75d4 100644
--- a/app/views.py
+++ b/app/views.py
@@ -1,6 +1,14 @@
+import logging
+import datetime
+from pyramid import httpexceptions
 from pyramid.view import(
     view_config
 )
+logger = logging.getLogger(__name__)
+
+
+# our memory-only pet storage
+PETS = {}
 
 
 @view_config(renderer='json', request_method='GET', route_name='app_views')
@@ -13,7 +21,9 @@ def get_pets(context, request):
         * 'animal_type'  -  `{"type": "string", "pattern": "^[a-zA-Z0-9]*$"}`
         * 'limit'  -  `{"type": "integer", "minimum": 0, "default": 100}`
     """
-    return {}
+    animal_type = request.GET.get("animal_type")
+    limit = request.GET.get("limit") or 100
+    return [pet for pet in PETS.values() if not animal_type or pet['animal_type'] == animal_type][:limit]
 
 
 @view_config(renderer='json', request_method='GET', route_name='app_views1')
@@ -25,7 +35,10 @@ def get_pet(context, request):
 
         * 'pet_id'  Pet's Unique identifier  `{"type": "string", "required": true, "pattern": "^[a-zA-Z0-9-]+$"}`
     """
-    return {}
+    pet_id = request.matchdict["pet_id"]
+    if pet_id not in PETS:
+        raise httpexceptions.HTTPNotFound()
+    return PETS[pet_id]
 
 
 @view_config(renderer='json', request_method='PUT', route_name='app_views1')
@@ -81,7 +94,19 @@ def put_pet(context, request):
         }
     ```
     """
-    return {}
+    pet_id = request.matchdict["pet_id"]
+    pet = request.json_body
+    exists = pet_id in PETS
+    pet['id'] = pet_id
+    if exists:
+        logger.info('Updating pet %s..', pet_id)
+        PETS[pet_id].update(pet)
+        return httpexceptions.HTTPOk()
+    else:
+        logger.info('Creating pet %s..', pet_id)
+        pet['created'] = datetime.datetime.utcnow()
+        PETS[pet_id] = pet
+        return httpexceptions.HTTPCreated()
 
 
 @view_config(renderer='json', request_method='DELETE', route_name='app_views1')
@@ -93,4 +118,10 @@ def delete_pet(context, request):
 
         * 'pet_id'  Pet's Unique identifier  `{"type": "string", "required": true, "pattern": "^[a-zA-Z0-9-]+$"}`
     """
-    return {}
+    pet_id = request.matchdict["pet_id"]
+    if pet_id in PETS:
+        logger.info('Deleting pet %s..', pet_id)
+        del PETS[pet_id]
+        raise httpexceptions.HTTPNoContent()
+    else:
+        raise httpexceptions.HTTPNotFound()

ついでに以下のような app/__init__.py も作ります。(defaultの状態ではdatetime.datetimeをJSONにserializeするところで失敗するのでadapterを追加してます)。

import logging
import os.path
from pyramid.config import Configurator
from pyramid.renderers import JSON
import datetime


def make_app(settings):
    config = Configurator(settings=settings)
    config.include("app.routes")

    # override: json renderer
    json_renderer = JSON()

    def datetime_adapter(obj, request):
        return obj.isoformat()
    json_renderer.add_adapter(datetime.datetime, datetime_adapter)
    config.add_renderer('json', json_renderer)

    return config.make_wsgi_app()


def main():
    from wsgiref.simple_server import make_server
    here = os.path.dirname(os.path.abspath(__file__))
    settings = {
        "here": here,
        "pyramid.reload_all": True,
    }
    app = make_app(settings)
    server = make_server('0.0.0.0', 8080, app)
    logging.basicConfig(level=logging.DEBUG)  # xxx
    server.serve_forever()


if __name__ == "__main__":
    main()

元のflaskのコードよりコード自体は増えていますが完成しました。実行してみます。

wget https://raw.githubusercontent.com/hjacobs/connexion-example/master/test.sh
--2017-01-03 20:09:28--  https://raw.githubusercontent.com/hjacobs/connexion-example/master/test.sh
Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 151.101.100.133
Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|151.101.100.133|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 388 [text/plain]
Saving to: ‘test.sh.2’

     0K                                                       100% 6.38M=0s

2017-01-03 20:09:28 (6.38 MB/s) - ‘test.sh’ saved [388/388]
$ bash test.sh
+ http PUT :8080/pets/1 name=foo animal_type=test
{"message": "\n\n\n\n\n", "title": "Created", "code": "201 Created"}+ http :8080/pets/1
{"created": "2017-01-03T11:10:14.379035", "name": "foo", "id": "1", "animal_type": "test"}+ http PUT :8080/pets/1 name=foo animal_type=test 'tags:={"color": "brown"}'
{"message": "\n\n\n\n\n", "title": "OK", "code": "200 OK"}+ http :8080/pets/1
{"tags": {"color": "brown"}, "name": "foo", "created": "2017-01-03T11:10:14.379035", "id": "1", "animal_type": "test"}+ http :8080/pets animal_type==test
[{"tags": {"color": "brown"}, "name": "foo", "created": "2017-01-03T11:10:14.379035", "id": "1", "animal_type": "test"}]+ http DELETE :8080/pets/1

変更後のコードにscaffoldを適用する

ところで route_nameがおかしいかんじです。もう少しまともな名前にしたいですね。

def includeme_swagger_router(config):
    config.add_route('app_views', '/pets')
    config.add_route('app_views1', '/pets/{pet_id}')
    config.scan('.views')

app_viewsをpetsにapp_views1をpetにしてみましょう。swagger.yamlに以下を追加します。 (x-pyramid-route-name に与えた文字列がrouteの定義に使われます。)

diff --git a/swagger.yaml b/swagger.yaml
index 3e0b59d..473d483 100644
--- a/swagger.yaml
+++ b/swagger.yaml
@@ -12,6 +12,7 @@ security:
   - oauth2: [uid]
 paths:
   /pets:
+    x-pyramid-route-name: pets
     get:
       tags: [Pets]
       operationId: app.views.get_pets
@@ -34,6 +35,7 @@ paths:
             items:
               $ref: '#/definitions/Pet'
   /pets/{pet_id}:
+    x-pyramid-route-name: pet
     get:
       tags: [Pets]
       operationId: app.views.get_pet

もう一度viewを再生成します。もちろん、先程手で書いたviewの本体のコードは壊れていません。

$ pyramid-swagger-router swagger.yaml .
 INFO:      pyramid_swagger_router.codegen:merge file: app/routes.py
 INFO:      pyramid_swagger_router.codegen:merge file: app/views.py
 INFO:                    prestring.output:touch directory path=./app
 INFO:                    prestring.output:touch file path=./app/routes.py
 INFO:                    prestring.output:touch file path=./app/views.py
diff --git a/app/routes.py b/app/routes.py
index 6fb6bc0..acb1e0c 100644
--- a/app/routes.py
+++ b/app/routes.py
@@ -1,6 +1,6 @@
 def includeme_swagger_router(config):
-    config.add_route('app_views', '/pets')
-    config.add_route('app_views1', '/pets/{pet_id}')
+    config.add_route('pets', '/pets')
+    config.add_route('pet', '/pets/{pet_id}')
     config.scan('.views')
 
 
diff --git a/app/views.py b/app/views.py
index 1cc75d4..7b502e5 100644
--- a/app/views.py
+++ b/app/views.py
@@ -11,7 +11,7 @@ logger = logging.getLogger(__name__)
 PETS = {}
 
 
-@view_config(renderer='json', request_method='GET', route_name='app_views')
+@view_config(renderer='json', request_method='GET', route_name='pets')
 def get_pets(context, request):
     """
     Get all pets
@@ -26,7 +26,7 @@ def get_pets(context, request):
     return [pet for pet in PETS.values() if not animal_type or pet['animal_type'] == animal_type][:limit]
 
 
-@view_config(renderer='json', request_method='GET', route_name='app_views1')
+@view_config(renderer='json', request_method='GET', route_name='pet')
 def get_pet(context, request):
     """
     Get a single pet
@@ -41,7 +41,7 @@ def get_pet(context, request):
     return PETS[pet_id]
 
 
-@view_config(renderer='json', request_method='PUT', route_name='app_views1')
+@view_config(renderer='json', request_method='PUT', route_name='pet')
 def put_pet(context, request):
     """
     Create or update a pet
@@ -109,7 +109,7 @@ def put_pet(context, request):
         return httpexceptions.HTTPCreated()
 
 
-@view_config(renderer='json', request_method='DELETE', route_name='app_views1')
+@view_config(renderer='json', request_method='DELETE', route_name='pet')
 def delete_pet(context, request):
     """
     Remove a pet

gist

先程のexampleをやった結果のgist です。

appendix:

datetime.datetimeの対応には、rendererの修正をしましたが。以前紹介した swagger-marshmallow-codegen を使うとserialize/deserializeが簡単になります。

2017年

方針とか目標とか思いを巡らしてみる気になった。

方針

できればこういう風に行動したいという感じのやつ。これは以下の様な感じのものにすることにした。

  • なるべくupstreamに還元する

いままではコードを書いていてちょっとした問題があった時に、面倒くさがって黒魔術だったりmonkey patchだったりで解決して済ましてしまうことが多かったのだけれど。まじめにissueを立てたりPRを出すというような普通に社会に存在している人の振る舞いと同様のことをして行きたいかもという気持ちがようやく芽生えてきた。いわゆるそーしゃる活動とか言うやつです。そういう活動をやっていけたら良いなという感じ。

(ところで、この新年で、既に、redbaronというライブラリで、バグに遭遇したあとmonkey patchで済ませてしまってissueも作っていないみたいなステータスのものがある。)

方針だけでは後でどうしようかということを考えるのが辛そうなので目標みたいなものも立てたほうが良いらしい。そんなわけで目標みたいな形でも出力してみる。

目標

あんまり具体的になる気がしないのでふわっとしたままの状態。

  • 普通の人と接するインターフェイスを整える
  • 何かのプライベートな何かに所属する
  • 月1位の平均でPR出す位はしたい(方針のやつ)
  • 盆栽的なプロジェクトを持つ

普通の人と接するインターフェイスを整える

普通の人と接するインターフェイスを整えると言うのは、時勢とのズレを認識位はしておいた方が良いかもと思ったりしたし。せめてvmみたいなもので仮想的に普通の人をエミューレーション出来る程度にはなっておいた方が良いかなみたいな気持ちになった。もちろん、エミューレーションは普通の人のように振る舞える様になるだけなので、素で普通の人と同じ土俵で競い合うと言うのは不可能だし。暫くすると消耗して継続不能になるという状態にはなると思う。

何かのプライベートな何かに所属する

気持ち的に一人でやるとやっぱり気力の消耗みたいなものが多い感じはしている。あと、何かしらのプライベートな進捗があった時に発表できるような場があるとやっぱり便利な感じはしている(主にプライベートでの開発の話し)。先天的な欠陥なのか何なのか、残念ながら、物理的な座標を指定されて時間通りに目的地に辿り着くみたいなタスクが異様に苦手で消耗してしまうので、できればオンラインの活動が主の場所に所属したい感じ。

あと、意外と、自分では進んで触らなかったことなどが時折社会との接続により必要になることがあったりして、その経験が積めると便利みたいなことはあったりするのでやっぱり社会との接続的な物がなくなると良くないんだな〜みたいな気持ちになったりしたりしている(苦労は買ってでもしたくないけれど)。例えば、何らかのサービスの管理みたいなことはどこかでやってみたい感じがしている。基本的にはサービスを作ったり運用したりという何かしらの依存が増えるような作業を避けていたのだけれど。ちょっとしたチューニングのベンチマークや思いつきを試す実験場として自分で好き勝手触れるサービス的なものが1つあると便利かなーみたいな思いが出てき始めた。

月1位の平均でPR出す位はしたい(方針のやつ)

方針のやつ。upstreamに還元するので一番分かりやすいのはPRなような気がする。その過程で何か所属できる場所も見つかるんじゃないかな〜みたいな気持ちも少しだけあったりする。世のボランティア的な感じで無償で色々頑張っている人達すごいな~と思いながら見てきたけれど。そういう人々のモチベーションの源泉などを観察して取り入れられそうなものがあったら取り入れるみたいなことができたら継続できるのかもしれないというところ(継続できなくても自分には不適みたいなことが分かるので良さそう)。

盆栽的なプロジェクトを持つ

最近気づいたのは、絵を描く人のらくがきみたいな作業が精神の健康を保つのにとても有効ということで。何らかの老廃物を外部に出力するために行う作業を持つということが、人々の一人として生きて行くには必要なことらしい。一般には創作活動と呼ばれているものがそれにあたる事が多いような気がしているけれど。個人的にはコードを書くことがそれに当たるらしい。日々仕事でコードを書いているから家でもコードを書かなくても大丈夫という感じは全くないっぽい。

直近の刹那的な楽しさはらくがきでも良いのだけれど。少しだけ労力をかけて変更を重ねていき、ある時ふと振り返って後ろを見てみた時に、何らかの蓄積が存在していると良いことが多い気がしている。進捗とか呼ばれている気がするけれど。この進捗があると無いのとでは精神的な余裕が違うような気がしている。つらい気持ちになりたくないので余裕を持ちたい。そんなわけで進捗を出したい。

あと、知らない人と出会った時に「何の人?(どんな人?)」という質問にも答えやすい感じがある。そんなわけで今まで色々なところに目移りをしていきながらすぐに飽きるみたいなことを繰り返してきた感じがあるけれど。余暇時間の内の幾分かを特定のものに注力してみるということをしてみたいと思った。

2016年の振り返り

はじめに

2016年の振り返りということをやってみる気になりました。今まで振り返りのような何かしらの人間味のある活動を避けたいという気持ちがあり。ただただ事実や試行の断片だけを出力していきたいという気持ちがあったのですが。それでは社会との接続が上手く行かないという感じがしてきはじめつつあり。人間味のある活動をやってみる気になり始めています。人間味のある人々は振り返りという名目で1年の間にどのようなことを行なっていたのか列挙してみてその時々に応じたコメントをするらしいので真似することにしました。

年初に書いていたことについて

今年の最初はバックエンドよりフロントエンドの方に力を入れるつもりだったようです。だいたいこのあたりの方針は4月位で転換してしまいました。この頃には陰も形なかったgoをやるみたいな感じになっています。数学関連云々は気力があればというようなオプショナルな形ですし。気力がなかったのでできませんでした。盆栽プロジェクト的なものは上手くできていないのですが。ここ最近はコードの生成にはまっているようでした。

自分用のメモを充実させることについて

今年の最初にあげたものの中では、自分用のメモを充実させるということに関してはうまくいっているような気がします。以前までは、gistにその時々の思いつきのコードを出力するだけで終わりにしていたのですが。文章を書くという行為や何かを説明するという行為が必要らしいということに気づき始めたのでした。

ところで文章というものは生産のコストが大きい。年の始めのころは真面目に文章を書くという意識でいたようなのですが、これを止めて自分用のメモの断片をgithub特定のリポジトリにあげるというような行為に変えたところこれがすこぶる良い感じで今に至っても続いています(githubの草が不当に生えるという不具合が発生していますが)。これはreadmeが勝手にそこそこ良い感じでレンダリングされるというのと、gistのフラットな構造から脱却できたところが大きい気がします。加えて、自分自身の試行の整理の備忘録に文章はあまり必要なかったようでした。

以下の事を念頭におくことを忘れないという部分について

念頭に置くことがどうとか色々言っていますが。まとめると好き勝手やるということで。一方で好き勝手やった結果のデメリットをなるべく受け持たないようにしようという行動指針っぽいなにかです。これはこれで良いなと思ったのですが。最近は社会的な何かとの関わりがそれなりに必要なのではないかということを思いはじめて、github上でissueを書くだったり、pull requestを送るというようなそーしゃる活動のようなことをしていったほうが良いみたいな感覚になり始めています。

今年作ったリポジトリ

今年作ったリポジトリは、adventカレンダーのものと先述したメモ用のリポジトリを除くと、17個位みたいです。その内作っただけに近いものが5個位なのでおそらく10個程増えた感じになりそうです。そう言えば、以前よりはforkだったりが増えてますね。先の17個からforkしたものなどは除いています。相も変わらず作っては放置みたいなやつが多そうです。あんまり何処かでリリースのアナウンス的なものもしていませんでした。

swagger-marshmallow-codegen

これは最近作ったやつですね。コード生成楽しいみたいなやつです。昨日眠れないついでに真面目にどういう状況なのか記事を書いています。

dictknife

これはdictの操作用のライブラリの寄せ集めです。意外と色々なところの内部で使っていて個人的には便利です。元々の発端みたいな記事をdict遊びという名前で書いていたようです。個人的にはこのdict遊びみたいな試みが好きですし。こういうことが好きな人と日常的に交流できそうな何処かに所属したいみたいな気持ちがあったりします。

例えば、json-referenceで分割されたJSONのファイル群を1つにまとめる処理などがこういう感じで書けます。あとdictの再帰的なmergeやdeepequalなどが地味に便利でした。 pip install dictknife[load] という感じでインストールすると、yamljsonのload,dumpの便利機能が使えるようになったりします。

swagger-bundler

分割されたswaggerのファイルをシュッと1つにまとめるやつです。これは最近新しい仕様で再実装したいので今はあんまり説明したくない感じです。次のバージョンでは、ファイルベースでの結合を止めて、真面目に名前空間をつける予定です。現在の挙動では何をimportしたのかわかんなくなるのが辛いという状況が発生しています。

goconvert

これは、goのコードを生成するためのpython用のライブラリ群です。主にgoの変換処理を生成しようと思って作業をしていた感じでした。作り途中です。qiitaに記事を書いていましたが全く興味は持たれなそうな感じでした。後述するgo-structjsonを使って、goのstruct定義をJSONファイルとして出力してあれこれやるみたいなやつです。

go-structjson

importのpathを見て再帰的にパッケージを見ながらAST使ってstruct定義をJSONとして抽出するみたいなやつです。goの情報を使ったコードをgo以外で書きたいと思ったので作りました。まじめに抽出する条件の絞込などができていなのでdefaultのexcludeの条件を外すとすぐに40000行とかのjsonを吐いたりしてしまいます。ツールとして作成した感じなのであんまりコードを綺麗に整える気にはなっていないので汚いです。

kanagata

これはpythonからmongodbのコードを触る時に、mongoengine位しか無さそうで。一方でmongoengineのdjangoのORMの方言みたいなインターフェイスがそもそもpython第二言語でしかない環境では無駄だろうという気持ちで作ったライブラリです。基本的には値の範囲を制限したdictとlistを作るライブラリです。ゆくゆくはswaggerの定義から自動生成をしようみたいなことを思っていましたが。今は止めてmarshmallowを使ってdictのvalidationをするだけで留めておこうみたいな気持ちになっています。もしかしたらmypyのtypeddictを使うというのが良いのかもしれません。

これを作った過程で、pythonのmongodbのclientのpymongoではcollections.UserDictは辞書として取り扱ってdeserialize/serializeできるのに対し、collecsions.UserListではエラーを吐くみたいな挙動になることがわかりました。jiraでチケットを作って(そういえばはじめてのjiraでした)、userlistに対応したpatchのようなものを投げてみましたが、adhocに個別に対応するのも馬鹿馬鹿しいしもっと汎用的な仕組みを後々用意するからそれで対応してみたいなことを言われたのであまり使い勝手が良くないです。(patchをあてないと、pymongoに値を受け渡すタイミングで、一度dictに変換する必要があります。)

kumonote

これは完全に途中で飽きて止めたリポジトリです。asyncioを使ってクローラーを作ろうと思ったらしいです。蜘蛛の手。

magicalimport

これは物理的なファイルパスをpythonで手軽にimportできるようにしようというライブラリです。plugin的なアーキテクチャを作る際に、わざわざパッケージを作らなくても済む様になるので意外と便利です。swagger-bundlerだったりswagger-marshmallow-codegenだったりの内部で使っています。

selfish

これはgoの練習用に作ったリポジトリです。gistにファイルをuploadするツールです。個人的にはこれでgistyを置き換えられたことでQoLが格段に向上しました。 具体的には-aliasというオプションを指定することで同一のgistに対する更新が手軽になったという辺りが大きいです。

# 最初のupload
$ selfish -alias head *.go
# 色々更新した後にgistに更新を反映
$ selfish -alias head *.go
# 新しいものを作るときには `-alias` を外すか別の名前で実行
$ selfish *.py readme.md

django-aggressivequery

これはdjangoのqueryのoptimizerです。後に触れるdjango-returnfielldsで生成するqueryをそこそこ良い感じにするために使っています。具体的にはN+1をjoinやprefetchに置き換えて除去してくれます。とは言え、職が変わったので、もう仕事でdjangoを使う事は無さそうです。なのであんまり積極的にメンテする気もない感じではあります。

django-returnfields

これはdjango restframework用のライブラリです。fat apiを定義したときのN+1クエリー的なものを除去するのに便利というやつです。そう言えばこれを作る過程で異様にdjangoのorm関連の記事を書いた記憶をがあります。

djangoオワコンなのか何なのか入門用の記事や特定のライブラリの紹介みたいな記事しか見かけないような気がします。

console-angular

これはangular1.xの挙動を確かめるためにhtmlやcssを書くのが馬鹿らしいと思って作ったパッケージだった記憶があります。内部的にはほとんど数行のラッパー。 元になった記事はこれっぽいです。

最近はangular1.x触っていないですし。今のおしごとのフロントエンドはangular(angular2)だったりですし。隔世の感があります。最近はフロントエンド触っていないので追いついていけてないですけれど。今だったらangularに限らずとりあえずbootcamp的にxxx-cliの内部を把握して、あとはテキトウに少しずつ新しい知識を集めていけば良いみたいなイメージでいます。

postcss-restructure

これはごみです。ほとんどpostcssのhello worldみたいなpluginな記憶。

cssconflict

これもごみです。これはcssのconflictした定義を見つけようと書き始めたものの、よく考えたらHTML部分も見なければレンダリングに使われる記述が決定できないみたいなことに気づいたみたいな経緯で放置されてます。

cssdiff

これはすごく巨大なcss同士の意味的なdiffを取ろうみたいなやつです。css parserでparseしたあとテキトウにdictでdiffを取るというだけなので数時間位で作った記憶があります。こういう便利ツールがstarを集めやすい(とは言え0件ではなく色がつくという程度)。

最後に

飽きました。(後で何か書く)