monogusaというパッケージを作りはじめた

github.com

monogusaというパッケージを作りはじめた。まだ完成には至っていないのだけれど現状でできることなどを書きながらここで使用感を確認していくことにした。

monogusa

monogusaというのはもちろん「ものぐさ」から来ている。お布団が大好きだったりこたつから出たくないような人々のこと。その場から動きたくない、手の届く範囲に望みのものが置かれて欲しいみたいな状況が目に浮かぶ。

今回はどういう感じのものを作ろうか?ということをお気持ちベースでメモに書いてみてから作ることにしてみた。

考えてみると、自分は自分の書くコードに対して、ドキュメントを書くということをサボりがちであるという認識を持っている。これが以下のどれなのかと言うのがはっきりしていない。

  • ドキュメントを書くのが苦手
  • 英語の文章を書くのが苦手 (英語でドキュメントを書くのが苦手)

後者は間違いないことではあるのだけれど。自己認識の上では前者かどうかは未知数という感じ。そういうわけで今回はもう少し文章や説明を多めに作ってみることにしてみている。このプロジェクトに関してはどうせ自分のためのコードなので英語でドキュメントを書く必要も無いような気がして来ている。

ポイントはコレ。

ある状態からある状態へ手軽に遷移できて欲しい

ここでの状態には色々な意味がある。詳しくはメモの方を参照してみると良いかもしれない(とはいえ自分のためのメモなので読んでも意味が読み取りづらいかもしれない)。

hello world

まだpipyにはあげていないのでテキトーにcloneしてきて pip install -e . をして見て欲しい。とりあえずインストールができたことにして筆を進める。

monogusaなので手軽になにかを行いたい。その何かは例えばコマンドとしての提供だったりする。手元で書いた関数がそのままコマンドとして提供されて欲しい。この機能だけを見るとhandofcatsと重なる部分があるのだけれど。monogusaはサブコマンドになる。

例えば以下の様な関数定義がある。

00cli.py

def hello(*, name: str) -> None:
    print(f"hello {name}")


def bye(*, name: str = "bye") -> None:
    print(f"bye {name}")

これをmonogusa越しに呼ぶとコマンドになる。

$ python -m monogusa.cli 00cli.py -h
usage: 00cli.py [-h] {hello,bye} ...

optional arguments:
  -h, --help   show this help message and exit

subcommands:
  {hello,bye}
    hello
    bye

hello, byeという関数がサブコマンド化されている。キーワード引数はフラグとして利用できるようになる。

$ python -m monogusa.cli 00cli.py hello --name=world
hello world

# help
$ python -m monogusa.cli 00cli.py hello -h
usage: 00cli.py hello [-h] --name NAME

optional arguments:
  -h, --help   show this help message and exit
  --name NAME

デフォルトキーワード引数はデフォルト値として扱われる (下の例では 'bye' がデフォルト値として使われる)。

$ python -m monogusa.cli 00cli.py bye
bye bye

$ python -m monogusa.cli 00cli.py bye -h
usage: 00cli.py bye [-h] [--name NAME]

optional arguments:
  -h, --help   show this help message and exit
  --name NAME  (default: 'bye') # <- default値

このままだとinvokeのようなタスクランナーと被るような所があるかもしれない。

docstring

もちろん関数のdocstringはhelp titleとして扱われる。例えば以下のようにdocstringを追加した時に。

--- 00cli.py 2019-12-16 23:05:53.016345987 +0900
+++ 01cli.py  2019-12-16 23:08:23.321588713 +0900
@@ -1,6 +1,8 @@
 def hello(*, name: str) -> None:
+    """hello message"""
     print(f"hello {name}")
 
 
 def bye(*, name: str = "bye") -> None:
+    """bye bye"""
     print(f"bye {name}")

help usageとして表示される。

$ python -m monogusa.cli 01cli.py -h
usage: 01cli.py [-h] {hello,bye} ...

optional arguments:
  -h, --help   show this help message and exit

subcommands:
  {hello,bye}
    hello      hello message # <- ここが追加されている
    bye        bye bye

async defされた関数

ちょっとしたおまけとしてasyncioの関数にも対応している。

02async-cli.py

import time
import asyncio


async def hello():
    print("hello", time.time())
    await asyncio.sleep(0.5)
    print("bye", time.time())

実行できる。

$ python -m monogusa.cli 02*.py hello
hello 1576505522.472876
bye 1576505522.9737217

DEBUG=1という形で環境変数に値を設定して実行してあげれば asyncio.run(<coroutine function>, debug=True) として実行される。

$ LOGGING=DEBUG DEBUG=1 python -m monogusa.cli 02async-cli.py hello
DEBUG:asyncio:Using selector: EpollSelector
hello 1576505750.2488549
bye 1576505750.7505808
DEBUG:asyncio:Close <_UnixSelectorEventLoop running=False closed=False debug=True>

この辺りのインターフェイスは変わるかもしれない。環境変数経由の値渡しは知らなければわからないインターフェイスなのでたくさんは増やさない予定。

positional arguments (component)

ところで今までの全部の関数定義の中でキーワード引数 (keyword only arguments) だけを使ってきた。では通常の引数 (positional arguments) は何に使われるのかというとDI的な機能として使われる。

例えば以下の様に、引数と同名のcomponent関数を用意してあげると自動的に埋め込まれて使われる。

03use-di.py

from monogusa import component


def hello(database_url: str) -> None:
    print(f"db from {database_url}")


@component
def database_url() -> str:
    return "sqlite:///:memory:"

例えば上の例では database_url が自動的に埋め込まれて使われる。

$ python -m monogusa.cli 03use-di.py hello
db from sqlite:///:memory:

この辺りは以下の機能を参考にして作られていた

このDI的な機能は再帰的に実行される。例えばhelloはDBに依存し、DBはdatabase_urlに依存している。

$ python -m monogusa.cli 04use-di.py hello
save hello message in sqlite:///:memory:

例えば以下の様なコードもOK。型ヒントは指定してあげる必要がある。

04use-di.py

from __future__ import annotations
from monogusa import component


def hello(db: DB) -> None:
    db.save("hello message")


@component
def database_url() -> str:
    return "sqlite:///:memory:"


class DB:
    def __init__(self, url) -> None:
        self.url = url

    def save(self, message: str) -> None:
        print(f"save {message} in {self.url}")


@component
def db(database_url: str) -> DB:
    return DB(database_url)

ただこれだけではまだ不足している気がしていて、lifecycle的なイベントが必要かもしれないと思っている。あとまだ他のfixture的な機能で用意されているgeneratorを利用したteardown付きのfixtureはサポートしてない。後々必要になるかもしれない。

web interface

ところでコレだけでは本当に価値があるとは思えない。それこそ単にコマンドを作れば良いだけな気がするし、タスクランナーからタスクを手元で実行すれば良いだけな気がする。

monogusaというのはこたつから出たくない人種、たとえばテレビのリモコンは手の届くところにあってほしい。良き所に良きものが置かれているという空気感をここでの文脈に持ってきたい。例えばweb APIとして提供できるようになっていると嬉しいかもしれない。

ここでwebAPIとして提供されるとはどういうことかといえば、消費者が自分自身から他の人へ広がるということになるのかもしれない。とりあえずはクローズドな環境でのことをイメージして見てほしい。

例えばfastAPIの上に乗ったglueコードを上手く取り扱うことができると、APIの機能の提供と同時にSwagger UI経由でbrowsable API的な機能が利用可能になる。この辺りはMicroBatchFrameworkの影響設けているかもしれない。

今はまだ手で書かなければいけない。けれど以下の様な感じで使える。

05web.py

$ python 05web.py -h
usage: 05web.py [-h] [--show-doc] [--debug] [--port PORT]

optional arguments:
  -h, --help   show this help message and exit
  --show-doc
  --debug
  --port PORT

直接実行するとuvicornが動く。

$ python 05web.py --port=55555
INFO:     Started server process [320004]
INFO:     Uvicorn running on http://127.0.0.1:55555 (Press CTRL+C to quit)
INFO:     Waiting for application startup.
INFO:     Application startup complete.

ここでAPI requestをしてみると返ってくる。コマンド実行の抽象化なのでstdoutとstderrがresponseとして返ってくる。そして全部POST。

$ echo '{"name": "world"}' | http --json POST :55555/hello
HTTP/1.1 200 OK
content-length: 101
content-type: application/json
date: Mon, 16 Dec 2019 14:57:20 GMT
server: uvicorn

{
    "duration": 2.3603439331054688e-05,
    "stderr": [],
    "stdout": [
        "save hello message in sqlite:///:memory:"
    ]
}

せっかくasgiなのでwebsocketなどのAPIを返したりすると良いのかもしれないと思ったりはしているが、ちょっとopenAPI docと相性が悪いような気がしている。

もちろん、fastAPIなので http://localhost:55555/docs などにアクセスすればSwagger UI経由でweb画面が見れる。

$ python -m webbrowser -t http://localhost:55555/docs

f:id:podhmo:20191217000728p:plain
swagger ui

ここまでを自動でできれば格好良いのだけれど。まだこの辺りは手書きする必要がある。現状は以下の様な形への変換が必要になる。DIなどはfastAPIのものを使う様に書き換えている。

05web.py

from __future__ import annotations
import typing as t
from fastapi import FastAPI, Depends
from pydantic import BaseModel
import monogusa.web.runtime as web


app = FastAPI()


class HelloInput(BaseModel):
    name: str


def database_url() -> str:
    return "sqlite:///:memory:"


class DB:
    def __init__(self, url) -> None:
        self.url = url

    def save(self, message: str) -> None:
        print(f"save {message} in {self.url}")


def db(database_url=Depends(database_url)) -> DB:
    return DB(database_url)


@app.post("/hello", response_model=web.CommandOutput)
def hello(input: HelloInput, db: DB = Depends(db)) -> t.Dict[str, t.Any]:
    with web.handle() as s:
        db.save("hello message")
        return s.dict()


if __name__ == "__main__":
    from monogusa.web import cli

    cli.run(app)

あるいは単にopenapi.jsonが欲しい場合には --show-doc 付きで実行してみれば良い。この辺りもfastAPIに乗っかることができればよしなにやってくれる。

$ python 05web.py --show-doc
openapi: 3.0.2
info:
  title: Fast API
  version: 0.1.0
paths:
  /hello:
    post:
      summary: Hello
      operationId: hello_hello_post
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/HelloInput'
        required: true
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommandOutput'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
components:
  schemas:
    CommandOutput:
      title: CommandOutput
      required:
      - stdout
      - stderr
      - duration
      type: object
      properties:
        stdout:
          title: Stdout
          anyOf:
          - type: array
            items:
              type: string
          - type: string
        stderr:
          title: Stderr
          anyOf:
          - type: array
            items:
              type: string
          - type: string
        duration:
          title: Duration
          type: number
    HTTPValidationError:
      title: HTTPValidationError
      type: object
      properties:
        detail:
          title: Detail
          type: array
          items:
            $ref: '#/components/schemas/ValidationError'
    HelloInput:
      title: HelloInput
      required:
      - name
      type: object
      properties:
        name:
          title: Name
          type: string
    ValidationError:
      title: ValidationError
      required:
      - loc
      - msg
      - type
      type: object
      properties:
        loc:
          title: Location
          type: array
          items:
            type: string
        msg:
          title: Message
          type: string
        type:
          title: Error Type
          type: string

その他先のこと

その他先のこととして、タスクキューとの組み合わせなどを考えている。非同期タスクを良い感じに取り扱いたい。それもローカル、オンプレ、クラウド上で良い感じに動くような形で。最初はSQS辺りを使うものかもしれない。あるいはredis経由でarqをwrapしたものになるかもしれない。これらの依存が動かなくては動かせないとだるいのでおそらくインメモリーの何かしらもローカルで動かせるようにすると思う。monogusaなので。

あとはbotとのつなぎ込みも手軽にできて欲しい。ある環境向けの閉じたwebAPIが個人の操作をチーム内に提供することだとしたら、botを通じたチャット経由でのUIを用意することはより制限を緩和化したゆるい関係にも機能を提供することにほかならない (民主化と言えば良いのかわからないがそういうような雰囲気の何か)。その辺りまで手軽に使える様になったら良いのかなーと思ったりしている。


  1. 元々はapistar (現 starlette ) 由来の機能。そして現在starlette自体にこの機能は無い。このあたりにはそこそこ複雑な経緯がある。

片手間にui componentsの使用感を試そうとしてやってみたことのメモ

片手間フロントエンドの人が、ui componentsの使用感を試すためにやろうとしたことのメモ。

はじめに

試そうとしたのは、reactのui components。以下2つ。

  • evergreen
  • tableau-ui

evergreen.segment.com github.com

テキトーにこの2つを選んだ。特にこだわりはない。まだこれら2つを使うと決めたわけではなく選考対象に乗せて使用感を試そうとしたくらいのステータス。

ちなみに選定基準は以下の2つ。

  • CSS書きたくない。
  • できればtagを書くだけでおしまいにしたい(web components的な)

そこそこ良い感じになっていれば良くて、細かな調整ができなくても良い。 まぁどれを選んだかは本題ではない。

使用感を試すために手元の環境で実際に触ってみたいというのがこの記事の趣旨。

触る方法

ちなみになぜ触りたいかと言うと、storybookなどを覗いて例を見るだけだと正直使用感のようなものがわからなかったから。

そして手元の環境で触ると言っても2通りくらい方法があるかもしれない。

  • ビルドを許容する方法 -- とりあえずビルドしても良いけれど。手間を少なめにしたい。
  • ビルドを許容しない方法 -- ビルドしたくない。絶対にビルドしたくない。

前者と後者の違いはnpmの環境を作るかどうか。別の言い方をするとビルドを許容するかどうか。

ビルドを許容する方法

ビルドを許容する方法はparcelを使うのが楽そうだった。

npmが存在する環境で以下の様な感じにやっていく。今回はevergreen-uiの方を試す。参考になりそうなページはparcelのreactのレシピ

$ npm init
$ npm install --save react react-dom
$ npm install --save evergreen-ui
$ npm install --save-dev parcel-bundler

package.jsonに以下を追加する。

  "scripts": {
    "start": "parcel serve index.html"
  },

あとはindex.html,index.css,index.jsをテキトウに書いて動かす(後述)

$ npm run start

> parcel serve index.html

Server running at http://localhost:1234 
...
✨  Built in 11.20s.

初回とはいえ10秒も掛かるのは毎回コレを動かすのは辛くなりそう。

表示された (hello world)。

f:id:podhmo:20191211183107p:plain

細々と思ったこと

  • 10秒も待ちたくない

watch (ファイル監視してのauto build) の機能がありそう。ただしこれはこれで現在の自分ののエディタ設定と相性が良くないかもしれない。自動でファイルを保存する設定と相性が良くないかもしれない。

あるいははもう少しparcelの中を覗いて小さくしたくなるかもしれない。

files

このとき作ったファイルのgist

https://gist.github.com/podhmo/92114a4fb7d486b7cd0035c41493eb51

index.js

import React from 'react'
import ReactDOM from 'react-dom'
import { Button } from 'evergreen-ui'


ReactDOM.render(
  <Button>I am using 🌲 Evergreen!</Button>,
  document.getElementById('root')
)

index.html

<!DOCTYPE html>
<html>
  <head>
    <title>Evergreen sandbox</title>
    <link rel="stylesheet" href="./index.css" type="text/css" />
  </head>
  <body>
    <div id="root"></div>
    <script src="index.js"></script>
  </body>
</html>

index.css

body {
    padding: 5rem;
}

Makefileからの実行

毎回package.jsonを作ってnpm startとやるのもめんどうなので同一パッケージ上でやる方法も少し調べた。npm installすると ./node_modules/.bin/parcel が存在するようになるのでコレを直接使う。

00:
    ./node_modules/.bin/parcel start $@index.html
01:
    ./node_modules/.bin/parcel start $@index.html
02:
    ./node_modules/.bin/parcel start $@index.html
03:
    ./node_modules/.bin/parcel start $@index.html

こんな感じに雑に書いてあげるとmake 0000index.htmlを利用する画面を確認できる。

とりあえずで使用感を試すときには幅優先的に探索をしたいので1つ前の状態を共有して色々試せるという環境が欲しくなる。

ビルドを許容しない方法

今度はビルドを許容しない方法。正確に言えばビルドを許容しないと言うよりはブラウザで完結させたいというような意味。色々調べてみるとunpkgを使う方法が見つかった。

unpkg.com

unpkg.com

unpkg is a fast, global content delivery network for everything on npm. Use it to quickly and easily load any file from any package using a URL like:

unpkg.com/:package@:version/:file

はい。

今度はindex.html一枚でやる方法を探してみる。evergreen-uiはちょっと例として適切ではなかったかもしれない。これを例にやろうとしたらめんどうだった。tableau-uiで試すことにする。

f:id:podhmo:20191211183154p:plain

具体的にどうめんどうだったかと言うと、jsのモジュールの種類としては以下の3つがあるらしいが、UMD形式のファイルがnpmリポジトリに直接入っているもの以外では手軽に扱うことができなそうだった。

UMDと言うと分かりづらいかもしれないけれど。全てのファイルを1つにまとめたもののこと。bundleされたもののこと。

詳しい話はこの辺りを参考にすると良さそう。

unpkg.com でtableau-uiを取り出そうとしてみる。

unpkgがいい感じに特定の形式のパスへとリダイレクトしてくれる模様。

$ http -b GET https://unpkg.com/@tableau/tableau-ui
Found. Redirecting to /@tableau/tableau-ui@2.2.1
$ http -b GET https://unpkg.com/@tableau/tableau-ui@2.2.1
Found. Redirecting to /@tableau/tableau-ui@2.2.1/./dist/tableau-ui.min.js
$ http -b GET https://unpkg.com/@tableau/tableau-ui@2.2.1/./dist/tableau-ui.min.js

ここで tableau-ui.min.js のものがUMD

index.htmlだけでhello world

unpkg.comを使うことでindex.htmlだけでhello worldすることができそう(中身は後述)。1つのファイルだけで済む。

基本的には以下の通り。

  • babel-standaloneを読み込んで、text/babel以下に書いたscriptタグのjsxを変換できるようにする
  • umd形式のものを読み込んでテキトウにprefix付きで使う (e.g. const Button = TableauUI.Button;)

細々と思ったこと

細々と思ったのは以下のようなこと

  • unpkg.comが重たい場合はキャッシュしたいかもしれない。
  • es6モジュールの形式のものも上手にhtmlを書いてあげれば上手くできたりしないんだろうか。
  • (babel-standaloneの形式が古くなったりすることはないんだろうか?)

files

gist

https://gist.github.com/podhmo/f2d623b474b8518f28f16d25d4172168

00index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <style type="text/css">
      body {
          padding: 5rem;
      }
    </style>
    <script src="https://unpkg.com/react@16/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/@tableau/tableau-ui"></script>
    <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script type="text/babel" defer data-presets="es2015,react">
const Button = TableauUI.Button;

function App(){
  return <div>
    <section><h2>enabled</h2>
    <Button>I am using tableau-ui</Button>
    <Button kind="primary">I am using tableau-ui</Button>
    <Button kind="outline">I am using tableau-ui</Button>
    <Button kind="destructive">I am using tableau-ui</Button>
    </section>
    <section><h2>disabled</h2>
    <Button disabled>I am using tableau-ui</Button>
    <Button disabled kind="primary">I am using tableau-ui</Button>
    <Button disabled kind="outline">I am using tableau-ui</Button>
    <Button disabled kind="destructive">I am using tableau-ui</Button>
    </section>
</div>
}

ReactDOM.render(
  <App/>,
  document.getElementById('root')
)
</script>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

misc

unpkgを手元で動かすのはどうするんだと調べてみた所。unpkg-serverなるパッケージがあるらしい。

この辺りのissueで紹介されていた。

-Self hosting? #98

コレを使ってunpkg.comの実行をwrapできないかとおもってテキトーなproxyをgoで書いた。こういうことが手軽にできるのはgoの強みだなーとは思う。今回は標準ライブラリしかまだ使っていない。

https://gist.github.com/podhmo/24e0cdb4d3a1288f59ba826f6925a09b

ただしまだnpmのregistryへのcacheが上手くできていなさそうで中身を見る必要があるかもしれない。

まとめ

ちょっとしたui componentの使用感を確認してみようとして幾つかのreactのui componentを試してみた。試す方法として以下2つの方法があった

  • ビルドを許容する方法
  • ビルドを許容しない方法

ビルドを許容する方法では、もう少しビルドの時間を短くしたいとおもった。なので課題としてはparcelの中身を覗くことやwatchと現在のエディタ設定との折り合いを付ける事。

ビルドを許容しない方法では、UMD以外の形式でのui componentsを試す方法を調べたいとおもった。後はキャッシュしてもう少し早い感じでできないかを試したかった。もしくは手元でビルドする環境のdocker imageを作ってあげてそいつにproxyしてあげればどうにかなるのかなーなどとおもったりした。

それぞれのgistはこのあたり