FastAPIでDI(Dependency Injection)したい
最近、FastAPIを使った開発をしたので、そこでやったことを書きます。
この記事は2020年のAdvent Calendarで書く予定の記事でしたが、間に合わず、2020年12月31日に投稿したものです…涙
使用技術
- Python 3.8
- FastAPI 0.61
- gunicorn 20.0.4
- uvicorn 0.12.2
- pydantic 1.7.1
実現したかった構成
依存関係を
view -> domain -> repository
という風に依存するようにしたかったです。 ※domain -> repositoryはこの記事では扱いません。別の記事に書こうかと考えています。
viewにdomainの依存性注入を試みました。
依存性注入のメリットについては、本記事では取り扱いません。気になる方は、こちらの記事が参考になりますのでぜひ読んでみてください。
困ったこと
Python製WEBフレームワークPyramidだと、add_request_method
があります。
add_request_method
を使って下記のような実装を実現できます。こうすることで、view層にdomainを依存させることができます。
この例だと、domainしか依存させていないので、依存させているdomainのみview層で直接呼び出せるようになりました。
domain.py
class Domain: def __init__(self): pass def add(self, a: int, b: int) -> int: return a + b def sub(self, a: int, b: int) -> int: return a - b
wsgi.py
from typing import Final import pyramid.config import pyramid.router import domain def _init_domain(config: pyramid.config.Configurator) -> None: def request_method() -> domain.Domain: return domain.Domain() config.add_request_method(request_method, 'domain', reify=True) def main() -> pyramid.router.Router: """ This function returns a Pyramid WSGI application. """ with pyramid.config.Configurator(settings=settings) as config: config.include('.views.routes') _init_domain(config) return config.make_wsgi_app()
views/__init__.py
import http import json from typing import Any, Dict, Final, cast import pydantic import pyramid class Error(Exception): pass class MissingParameterError(Error): pass class Addiction(pydantic.BaseModel): augend: int addend: int class Subtraction(pydantic.BaseModel): subtrahend: int minuend: int def _take_json(request: pyramid.request.Request) -> Dict[str, Any]: try: return cast(Dict[str, Any], request.json) except json.JSONDecodeError: # Request BodyがJSONでない場合に起きます。 raise MissingParameterError() def add( request: pyramid.request.Request) -> pyramid.response.Response: parameter: Final = Addiction.parse_obj(_take_json(request)) result: Final = request.domain.add(parameter.augend, parameter.addend) return pyramid.response.Response( status=http.HTTPStatus.OK, json={'result': result}, ) def sub( request: pyramid.request.Request) -> pyramid.response.Response: parameter: Final = Subtraction.parse_obj(_take_json(request)) result: Final = request.domain.sub(parameter.subtrahend, parameter.minuend) return pyramid.response.Response( status=http.HTTPStatus.OK, json={'result': result}, )
views/routes.py
import pyramid.config import views def includeme(config: pyramid.config.Configurator) -> None: for pattern, view in [ ('/add', views.add), ('/sub', views.sub), ]: config.add_route(pattern, pattern, request_method='POST') config.add_view(view, route_name=pattern, request_method='POST')
起動コマンド
poetry run waitress-serve --call wsgi.main
FastAPIでこれ相当のことをする方法を探すのに苦戦しました。それで、今回の記事を書こうと決めました。
Domainオブジェクト→view層に依存を実現するためのDepends
View層に直接依存するものは、FastAPIの機能の一つ、Dependsを使いました。
Dependencies - First Steps - FastAPI
Dependsは、DIを実現するために、FastAPIが用意してくれている仕組みです。
views.py
from typing import Final import fastapi import fastapi.responses import pydantic import domain # 内容は上記のdomain/__init__.pyと同じです。 def _domain_factory() -> domain.Domain: return domain.Domain() class Addiction(pydantic.BaseModel): augend: int addend: int def add( addiction: Addiction, # FastAPIでは、request bodyとなるオブジェクトを引数で受け取ります。 domain: domain.Domain = fastapi.Depends(_domain_factory), # domain.Domainを返す関数をDependsに渡します。 ) -> fastapi.responses.JSONResponse: result: Final = domain.add(addiction.augend, addiction.addend) return fastapi.responses.JSONResponse({'result': result})
routes.py
import fastapi import views def add_routes(app: fastapi.FastAPI) -> None: app.add_api_route( "/add", views.add, methods=["POST"], ) app.add_api_route( "/sub", views.sub, methods=["POST"], )
wsgi.py
from typing import Final import fastapi import routing def main() -> fastapi.FastAPI: app: Final = fastapi.FastAPI() routing.add_routes(app) return app
起動コマンド
gunicorn 'app.wsgi:main()' -k uvicorn.workers.UvicornWorker
view層の関数の引数として、依存させたいオブジェクトを渡します。 引数の名前は、view層の関数内で利用したい変数名にします。
注意しないといけないのは、Dependsにdomain.Domainを返す関数を渡すことです。(理由は分かりませんが、Domainに引数となる値を渡す場合、関数を渡さないと関数定義時の値が評価されるからでしょうか? 実行時の値が評価されないと、日時オブジェクトなどは意図しない値になりますからね)
FastAPIでもPyramidのように、view -> domainの依存性注入を実現できることが分かりました。
実際のプロジェクトでは domain -> repositoryの依存性注入もしましたが、長くなってしまうので別の記事に分けて書こうかと思います。