👏

2020年にPythonでWebフレームワークを自作した話

2020/11/28に公開

「PythonでのWeb開発はFlaskかDjangoでしょ」と言われる時代かと思いますが、今更ながらWebフレームワーク 「Mitama」 を自作しました。
https://github.com/mitama-org/mitama
といっても、ただオレオレFWを組んでツヨツヨを装いたかったというわけではなく、拡張性の高い社内向けの汎用Webシステムを作るためのアプローチとしてフレームワークという形を取ったという運びです。
この記事では、そんな少し特殊なフレームワークを組む上で意識したことをまとめます。

(ブログに「なんでこんなものを作ることになったのか?」という経緯の話を書いてみたので、よかったらそちらも読んでみてください(^^) → https://boke0.netlify.app/blog/mitama-story/

どんなFWを作ったのか?

どんなものかを一言で表すことが難しいので、まずはワークフローを示したいと思います。

まず、Mitamaのアプリケーションを動かすためにmitama.jsonという名前のファイルを設置したディレクトリを用意します。これを 「プロジェクト」 と呼ぶことにします。

mitama.jsonは起動したいPythonパッケージの名前と配信先のパスを対応付けて記述します。
この時、プロジェクトのディレクトリもPythonのパッケージのimport先のパスとして追加されているので、ここにPythonパッケージを直接設置することで自作アプリを簡単に設置することができます。

mitama.jsonを記述したら、mitama runコマンドを叩いてwsgiref.SimpleServerを起動するか、uwsgiの設定を行い、起動する形でサーバーを動かします。これによって、先程対応付けたパス以下にアクセスすると、指定されたパッケージが起動するようになります。

Djangoとの違い

これだとDjangoの劣化版みたいですが、Djangoと大きく違う点はデータベースの扱い方です。

DjangoはWebサービスを開発するために設計されているためか、複数のDjangoアプリを同じプロジェクトで使用する場合でもデータベースは基本的に一つだけですが、Mitamaではユーザーや組織情報を管理するデータベースがプロジェクトディレクトリ下に、その他のアプリが扱うデータは プロジェクトディレクトリ下に生成されたアプリ用のディレクトリにデータベースが設置され、その中に入ります

上の図の場合だと、下図のような配置になります。

project_dir/
|- chat/
|  |- __init__.py
|  +- db.sqlite3
|- settleman-app/
|  |- __init__.py
|  +- db.sqlite3
|- mitama.portal/
|  +- db.sqlite3
+- db.sqlite3

なぜこんな設計にしたかと言うと、社内向けのシステムという背景から、気になったアプリを即入れて試し、気に食わなかったらさっさとアンインストールするような代謝がほしかったためです。データベースにゴミが残らないように、プロジェクト下に生成されるアプリ用のディレクトリを削除してしまえばデータベースごと消える様にしました。

意識したこと

環境による不具合

Mitamaを作るにあたってDjangoやFlask、Bottle、aiohttpなど、いろいろなPython製ウェブFWのコードを読み漁ったのですが、やはりどれもuWSGIやFastCGIで動かすときなどの細かな調整は開発者がアプリを書く時に行う必要がありました。

しかし、Mitamaの場合はuWSGIだろうがFastCGIだろうがSimpleServerだろうがお構いなしに動いてほしかったため、なるべくどんな環境でも動くようにこちら側から調整してやるべきだと思いました。

特にセッション管理の実装をしているときには、当初Cookieに突っ込むセッション情報を暗号化する鍵を一時変数に保持しており、SimpleServerでは上手く動作したものがuWSGIでは上手く動かないといった現象が起こりました。(結局鍵はファイルに吐きました)

ササッとモデルを組める仕様

Mitamaはしっかり目にMVCアーキテクチャに従っていて、データベース周りの仕様はFlask SQLAlchemyを丸パクリしているのですが、自分なりに簡単に書けるように工夫してみました。
たとえば、社内Twitterを作りたいとすると、以下の様にモデルを組むことができます。

...
db = Database()
class Tweet(db.Model):
	user = Column(User)
	content = Column(String)
	datetime = Column(DateTime)
class Like(db.Modle):
	tweet = Column(Tweet.type)
	user = Column(User)
	datetime = Column(DateTime)
...

こうすることで、例えば以下のようなことができます。

like = Like.retrieve(int(request.params['id'])) #URLパラメータからLikeを取得
print(like.tweet.content) #いいねしたtweetのcontent

SQLAlchemyにもrelationという機能があるにはあるのですが、自分にとって書きやすいのを作ってみました。ここらへんの機能はもうちょっと拡張していきたいです。

終わりに

かなり今どきじゃないことをやっていますが、それなりに使えるものを作ったと自負してます。いかがでしょうか。
「どんなソフトウェアがあり、そこで何ができるのか」という考え方がされがちですが、「何をしたく、そのために何を作るか」という発想がより簡単に実現できたら良いなと思ってます。

よかったらリポジトリにStarください!
質問、疑問、文句、プルリク、お待ちしてます!

Discussion