monogusaというパッケージを作りはじめた
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:
この辺りは以下の機能を参考にして作られていた
- pytestのfixtureの機能
- fastAPIのDIの機能1
この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
ここまでを自動でできれば格好良いのだけれど。まだこの辺りは手書きする必要がある。現状は以下の様な形への変換が必要になる。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を用意することはより制限を緩和化したゆるい関係にも機能を提供することにほかならない (民主化と言えば良いのかわからないがそういうような雰囲気の何か)。その辺りまで手軽に使える様になったら良いのかなーと思ったりしている。
片手間に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)。
細々と思ったこと
- 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 00
で00index.html
を利用する画面を確認できる。
とりあえずで使用感を試すときには幅優先的に探索をしたいので1つ前の状態を共有して色々試せるという環境が欲しくなる。
ビルドを許容しない方法
今度はビルドを許容しない方法。正確に言えばビルドを許容しないと言うよりはブラウザで完結させたいというような意味。色々調べてみるとunpkgを使う方法が見つかった。
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で試すことにする。
具体的にどうめんどうだったかと言うと、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で紹介されていた。
コレを使って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はこのあたり