pythonのslack関連のパッケージはどの辺りを見れば良いか調べた
pythonのslack関連のパッケージはどの辺りを見れば良いか調べた。古の記憶ではslackbotが有名だった。
過去の自分はえらいので以前にpytestのpluginについて調べたものと同様の方法を使えば良さそう。
pypistatsとか名前忘れていた。正直。
試す
だいたい以下の感じで進めれば良い
- pip search でテキトーに関連しそうなパッケージの一覧を探す
- pypistatsでテキトーにデータを取ってくる
- 月間ダウンロード数あたりでソート。
- 上位N件くらいを見る
はい。
実際にやってみる
手順は以下のMakefileがすべて。00から順に実行していけば良い。pypistatsとjqfpyは必要。
# 一覧取得 00: pip search slack | cut -d " " -f 1 | grep -v django | tee $@.txt pip search slackbot | cut -d " " -f 1 | grep -v django | grep -v markov_slackbot >> $@.txt # pypistatsにアクセス 01: for i in `cat 00.txt`; do pypistats recent $$i -f json; done | tee $@.json # 月間ダウンロード数でソート 02: cat 01.json | jqfpy --slurp 'sorted(get(), key=lambda x: x["data"]["last_month"], reverse=True)' --squash -c | tee $@.json # gistで見る分にはCSVが便利 03: echo package,download_last_month > 03.csv cat 02.json | jqfpy --slurp '[print(f"""{d["package"]},{d["data"]["last_month"]}""") for d in get()]; None' >> 03.csv # markdownでみたいよね 04: dictknife cat -o md 03.csv | tee $@.md
はい。こういうことが10分くらいでできたので良い。
結果
結果は、slackbotはやっぱり強くてslack-webhook-cliとslack,dagster-slack, slack-webhook, celey-slack, slack-cli辺りを見ておけば十分そう。
詳しくはこういう感じ。途中で切った。
package | download_last_month |
---|---|
slack-webhook-cli | 53994 |
slackbot | 18247 |
slack | 9564 |
pytest-slack | 9475 |
slack-logger | 5790 |
dagster-slack | 3088 |
slack-webhook | 2077 |
celery-slack | 1833 |
slack-cli | 1646 |
fabric-slack-tools | 1590 |
slack-entities | 842 |
slack-client | 825 |
追記
価値がありそうなのは以下くらいかも
- slackbot -- Based on slack Real Time Messaging API な所だけ注意だけれど。やっぱりコレが無難そう。単にpostするだけならrequestsで十分かも。
- python-slack-logger -- loggerと繋げてるだけだけど。場合によっては便利かもしれない。ちょっとしたhighlight付きなので。
- dagster-slack -- そもそもdagsterが何者?という感じなので後で調べる必要はある。覗いてみても良いかもくらい。
- slack-cli -- 人によってはCLIとして便利なことはあるかも。ちょっとした装飾を付けたい場合にも参考にするのはあり(中のコード覗いて無いけど)。
細々と想ったこと
ここからは細々とおもったこと
monthlyのダウンロードだけだとだるいかも?
けっこうgithubのstar数とはかけ離れていそう。けっこうスター数0のものがある。できれば1個は付いていて欲しい(?)。 あと、dagster-slackとか本体の方のスター数が見えるんだなー。pypi上での遷移先では。
あと、release_historyも欲しい。直近の更新がない場合は基本的に切るので(ただ月次のダウンロード数が多い状況なら安定しているのかもしれない)。
pip search?
なるほどと想ったのは、pip search slack
でslackbotが含まれないこと。へー。と思ったりした。
$ pip -V pip 19.3.1 from $HOME/venvs/my/lib/python3.8/site-packages/pip (python 3.8)
$ pip search slack | grep slackbot | wc 0 0 0 $ pip search slackbot | grep slackbot | wc 23 242 1965
pypistats
キャッシュは効くものの1回1秒程度は掛かる。もう少し頻繁にこの種の作業をするとしたらpypistatsを雑にCLIで使うという行為は卒業しても良いかもしれない。今はまだ大丈夫だと思っている。
URL
(追記:) こういう記事を書くのはあまり意味が無いと思ったけれど。リンク先をポチポチとクリックして遷移できるのはけっこう便利だった。
pypistatsの詳細ページのリンクもあると嬉しいかもしれない。例えば
- pypi -- https://pypi.org/project/slack-webhook-cli/
- pypistats -- https://pypistats.org/packages/slack-webhook-cli
- Homepage -- https://github.com/philippbosch/slack-webhook-cli
そしてpypiにreadmeを載せていないページもあるのでホームページのリンクもあると良いかもしれない。次回の話。
gist
gist
https://gist.github.com/podhmo/b841bac22e75b6fa8acc3d8f3503ff67
monogusaのweb周りのコードを自動生成できるようにした
monogusaのweb周りを更新した。大きな変更は前回からweb側のコードも生成されるようになったこと。前回の記事では「仮にこのように書けるとしたら...」という形でweb側のコードは手書きでの解説になったがそこは生成されるようになった。
関数定義からのweb UI生成
例えば以下の様な関数定義を持つcommands.pyがある。
commands.py
def hello(*, name="world"): print(f"hello {name}")
これに対して monogusa.web
を実行する(この辺りのCLIのインターフェイスは後々変わることがありそう。まぁexperimentalなので)。すると以下の様なweb.pyが生成される。
web.py
$ python -m monogusa.web commands.py
[F] create $HOME/venvs/my/individual-sandbox/daily/20191225/example_monogusa/01web/web.py
生成されるコードについては後に触れるが、これは概ね前回の記事で説明に使った記事と同様のもの。当然同様に実行が可能。
サーバーが立ち上がる。
$ python web.py Now go to http://127.0.0.1:55555/docs. You will see the automatic interactive API documentation INFO: Started server process [43110] INFO: Uvicorn running on http://127.0.0.1:55555 (Press CTRL+C to quit) INFO: Waiting for application startup. INFO: Application startup complete.
そして、/docs
にアクセスすればSwagger UI経由のbrowsable APIが手にはいる。
あともちろんAPIが提供されることになるので POST /hello
などとリクエストすると、元の関数がふつうに呼ばれる。ただしコマンド実行を模したものなのでstdout,stderrをresponseとして返す。1
$ echo '{"name": "foo"}' | http --json post :55555/hello HTTP/1.1 200 OK content-length: 88 content-type: application/json date: Wed, 25 Dec 2019 06:31:29 GMT server: uvicorn { "duration": 2.7179718017578125e-05, "status_code": 0, "stderr": [], "stdout": [ "hello world" ] }
生成されるコードは以下の通り。基本的にfastAPIに依存している。
web.py
# this module is generated by monogusa.web.codegen import commands from fastapi import ( APIRouter, Depends, FastAPI ) import typing as t from pydantic import BaseModel from monogusa.web import runtime router = APIRouter() @router.post("/hello", response_model=runtime.CommandOutput) def hello() -> t.Dict[str, t.Any]: with runtime.handle() as s: commands.hello() return s.dict() def main(app: FastAPI): from monogusa.web import cli cli.run(app) app = FastAPI() app.include_router(router) if __name__ == '__main__': main(app=app)
現在は1ファイルにまとめただけの出力だが、asgi appやmainは生成せずにrouter部分だけ生成するようにしても良いかもしれない。
引数指定が不要なもの
(これは趣味の範囲ではあるけれど)現在の実装ではキーワード引数がある関数はすべてJSON bodyとして扱われてrequestから渡されることを期待している。コマンドの実行を公開するという行為に絞ったわりきりなのでRESTだからどうとか考えることは辞めた。
キーワード引数が無い以下の様な関数などは以下の様に出力される。
commands.py
def cleanup_db(): print("clean-up")
web.py (の一部)
@router.post("/cleanup_db", response_model=runtime.CommandOutput) def cleanup_db() -> t.Dict[str, t.Any]: with runtime.handle() as s: commands.cleanup_db() return s.dict()
同様にPOSTでアクセスできるがbodyは不要。
$ python -m monogusa.web commands.py [F] create $HOME/venvs/my/individual-sandbox/daily/20191225/example_monogusa/02nokwargs/web.py $ python web.py ...
別のシェルで
$ http POST :55555/cleanup_db HTTP/1.1 200 OK content-length: 84 content-type: application/json date: Wed, 25 Dec 2019 06:42:06 GMT server: uvicorn { "duration": 4.839897155761719e-05, "status_code": 0, "stderr": [], "stdout": [ "clean-up" ] }
常にPOSTなのでGETはmethod not allowed。
$ http :55555/cleanup_db HTTP/1.1 405 Method Not Allowed content-length: 31 content-type: application/json date: Wed, 25 Dec 2019 06:42:01 GMT server: uvicorn { "detail": "Method Not Allowed" }
依存のあるもの
前回の記事でも言及したように2 positional argumentsは引数名と同名の関数で提供されるコンポーネントへの依存として扱われる。依存は実行時に解決される。これはfastAPIのDIの機能の上に乗るように調整されたコードが返ってくる。
例えば以下の様なコードを用意する。helloはdbにdbはdatabase_urlに依存している。
commands.py
from __future__ import annotations import dataclasses from monogusa import component def hello(db: DB, *, name: str = "world") -> None: db.save(f"hello {name}") @component def database_url() -> str: return "sqlite:///:memory:" @component def db(database_url: str) -> DB: return DB(database_url) @dataclasses.dataclass class DB: database_url: str def save(self, msg: str) -> None: print(f"save: {msg} in {self.database_url!r}")
これは以下の様なコードが生成される(必要な部分だけを抜粋)。
import commands def db(database_url: str=Depends(commands.database_url)) -> commands.DB: return commands.db(database_url) router = APIRouter() class HelloInput(BaseModel): name: str = 'world' @router.post("/hello", response_model=runtime.CommandOutput) def hello(input: HelloInput, db: commands.DB=Depends(db)) -> t.Dict[str, t.Any]: with runtime.handle() as s: commands.hello(db, **input.dict()) return s.dict()
そのまま利用できるものは直接 Depends(...)
を使い、依存がfastAPI上で定義されて欲しいものは別途関数を生成している。fastAPIは非同期の依存も許容するのでmonogusaの方も非同期の依存を許容するように後で書き換えたい。
もちろんしっかり動作する。
$ echo '{}' | http --json POST :55555/hello HTTP/1.1 200 OK content-length: 117 content-type: application/json date: Wed, 25 Dec 2019 06:53:38 GMT server: uvicorn { "duration": 3.361701965332031e-05, "status_code": 0, "stderr": [], "stdout": [ "save: hello world in 'sqlite:///:memory:'" ] }
エラーが発生した場合
エラーが発生した場合はスタックトレースがそのままstderrの中に格納されたresponseが返ってくる。CLI上での実行を模した動作を目指していたため。このあたりは本番環境で有効になっているとセキュリティ的に不安のある動作ではあるけれど、そもそも内部の閉じられた環境での一時的な機能提供をユースケースにあげていたので。
例えば雑なzero division errorを。
commands.py
def must_error(): print("start") 1 / 0 print("NEVER")
実行してみる。
$ python -m monogusa.web commands.py [F] create $HOME/venvs/my/individual-sandbox/daily/20191225/example_monogusa/04must_error/web.py $ python web.py
別のシェルで
$ echo '{}' | http --json POST :55555/must_error HTTP/1.1 200 OK content-length: 390 content-type: application/json date: Wed, 25 Dec 2019 06:59:46 GMT server: uvicorn { "duration": 0.0005054473876953125, "status_code": 1, "stderr": [ "Traceback (most recent call last):", " File \"$HOME/venvs/my/monogusa/monogusa/web/runtime.py\", line 40, in handle", " yield s", " File \"./web.py\", line 19, in must_error", " commands.must_error()", " File \"./commands.py\", line 3, in must_error", " 1 / 0", "ZeroDivisionError: division by zero" ], "stdout": [ "start" ] }
ところでhttp statusは200のまま。responseに含まれるstatus_codeは0ではなく1になっている。
ここはhttp status 500を返すように変更しても良いかもしれないが、fastAPI側のコードとの兼ね合いなどを考えて決めあぐねている(正確に言えばopenapi docとの兼ね合いかもしれない。500のときのresponseの形状が複数になるというのがだるいかもしれないという話も含んでいるので)
web.pyの位置を変える
今までの例は提供元の関数を定義しているcommands.pyと同一の階層上にweb.pyを出力していたが、別の場所に出力することも可能。その場合はmagicalimportというパッケージを経由してimportされるようなコードが生成される。
--dst
オプションで出力先を指定する。
$ python -m monogusa.web commands.py --dst /tmp [F] update /tmp/web.py $ diff -u web.py /tmp/web.py
以下の様な差分が出る。
--- web.py 2019-12-25 15:58:45.844393161 +0900 +++ /tmp/web.py 2019-12-25 16:04:07.393448395 +0900 @@ -1,5 +1,5 @@ # this module is generated by monogusa.web.codegen -import commands +import magicalimport from fastapi import ( APIRouter, Depends, @@ -10,6 +10,9 @@ from monogusa.web import runtime +commands = magicalimport.import_module('$HOME/venvs/my/individual-sandbox/daily/20191225/example_monogusa/04must_error/commands.py', cwd=True)
同一階層以外は全て絶対パスを埋め込んだコードを生成するような割り切った実装。この辺は柔軟に指定できるようにするか、いっそのこと出力先を固定するための設定ファイルを用意するべきかということは決めあぐねている。
まとめ
変更点は以下
- web.pyのコードを生成できるようにした
- DIも気にするようにした
- エラーのときにはstderrにスタックトレースが入る
- どこにでも出力できる。
最後に
monogusa.webの方でコード生成するようにしたが、その過程で頑張ってしまったのでCLI側でも生成するようにしても良いかもしれない。ui的には統一的に扱えるようにしたほうが良い気はしている。デフォルトでは生成するだけにして --eval
オプションを用意するか、またはデフォルトは/tmp的なディレクトリに生成してしまって常にevalするかというところが悩みどころ。
ただ、今回はwebだけの更新になってしまったが、元々の提供範囲としてはcliとwebだけに限ったものではない。コードのrefineなどはゆっくりやれることではあるので、どういう機能を作るかの試行錯誤にも時間を振った方が良い気がしている。