4
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Python Fast APIで依存性注入をどう使うかまとめてみた

Posted at

Classes as Dependencies

依存性注入に学ぶ前に、前回の例をアップグレードしてみましょう。

A dict from the previous example

前の例では、依存関係("dependable")からdictを返していました。

from typing import Optional

from fastapi import Depends, FastAPI

app = FastAPI()


async def common_parameters(q: Optional[str] = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
    return commons


@app.get("/users/")
async def read_users(commons: dict = Depends(common_parameters)):
    return commons
  • しかし、パス操作関数のパラメータコモンズでディクトを取得してしまいます。エディタはキーと値の型を知ることができないので、ディクトに対して多くのサポート(補完など)を提供できないことがわかっています。私たちはもっと良いことができます...。

依存関係を作るものは何か

  • これまでは、依存関係が関数として宣言されているのを見てきました。しかし、依存関係を宣言する唯一の方法ではありません(おそらくその方が一般的でしょうが)。重要なのは、依存関係は "callable "でなければならないということです。
  • Pythonにおける "callable "とは、Pythonが関数のように "呼び出せる "ものを指します。つまり、(関数ではないかもしれませんが)何かのオブジェクトを持っていて、それを「呼び出せる」(実行できる)場合は、以下のようになります。
something()

Classes as dependencies

  • Pythonクラスのインスタンスを作成する際に、同じ構文を使用していることに気づくかもしれません。
class Cat:
    def __init__(self, name: str):
        self.name = name


fluffy = Cat(name="Mr Fluffy")
  • この場合、fluffyはCatクラスのインスタンスです。そして、fluffyを作成するために、Catを「呼び出し」していることになります。
  • つまり、Pythonクラスも呼び出し可能なクラスということになります。そして、FastAPIでは、Pythonクラスを依存関係として使うことができます。
  • FastAPIが実際にチェックしているのは、「callable」(関数でもクラスでも何でもいい)であることと、パラメータが定義されているかどうかです。astAPIで依存関係として「callable」を渡すと、その「callable」のパラメータを解析し、サブ依存関係を含みパス操作関数のパラメータと同じように処理してくれます。これは、パラメータが全くないコールアブルにも適用されます。パラメータを持たないパス操作関数の場合と同じです。
  • あとは、上で紹介した依存関係の「依存性のある」common_parametersをCommonQueryParamsクラスに変更すればOKです。
  • これらのパラメータは、FastAPI が依存関係を「解決」するために使用するものです。どちらの場合も、
    • オプションの q クエリパラメータ。
    • スキップクエリパラメータ、デフォルトは 0 です。
    • limit クエリパラメータ、デフォルトは 100 です。
      どちらの場合も、データは変換され、検証され、OpenAPI スキーマ上で文書化されます。
  • FastAPIはCommonQueryParamsクラスを呼び出します。これにより、そのクラスの「インスタンス」が作成され、インスタンスはパラメータコモンズとして関数に渡されます。
@app.get("/items/")
async def read_items(commons: CommonQueryParams = Depends(CommonQueryParams)):

Type annotation vs Depends

上記のコードで CommonQueryParams を 2 回記述していることに注目してください。

commons: CommonQueryParams = Depends(CommonQueryParams)

これでも意味は同じです

commons = Depends(CommonQueryParams)
  • しかし、型を宣言することは推奨されています。そうすれば、エディタはパラメータコモンズとして何が渡されるかを知ることができ、コードの補完や型チェックなどに役立ちます。
  • ここではCommonQueryParamsを2回書いているため、いくつかのコードが繰り返されていることがわかります。
  • これらの特定のケースでは、次のようにします。
commons: CommonQueryParams = Depends()
  • パラメータの型として依存性を宣言し、Depends() の中でパラメータを指定せずに、その関数のパラメータの "デフォルト" 値 (= の後の値) として Depends() を使用することで、Depends(CommonQueryParams) の中でクラス全体を書き直す必要がなくなります。同じ例は次のようになります。
@app.get("/items/")
async def read_items(commons: CommonQueryParams = Depends()):

Sub-dependencies

  • サブ依存関係を持つ依存関係を作成することができます。それらは必要なだけ深いものにすることができます。FastAPIはそれらの解決の世話をします。最初の依存関係のような最初の依存関係("dependable")を作ることができます。
from typing import Optional

from fastapi import Cookie, Depends, FastAPI

app = FastAPI()


def query_extractor(q: Optional[str] = None):
    return q


def query_or_cookie_extractor(
    q: str = Depends(query_extractor), last_query: Optional[str] = Cookie(None)
):
    if not q:
        return last_query
    return q


@app.get("/items/")
async def read_query(query_or_default: str = Depends(query_or_cookie_extractor)):
    return {"q_or_cookie": query_or_default}
  • これはオプションのクエリパラメータqをstrとして宣言し、それを返すだけです。これは非常に単純ですが(あまり便利ではありません)、サブ依存関係がどのように動作するかに焦点を当てるのに役立ちます。
  • 宣言されたパラメータに注目してみましょう。
  • この関数は依存関係(「依存可能」)そのものであるにもかかわらず、別の依存関係を宣言しています(何か他のものに「依存」しています)。この関数はquery_extractorに依存しており、それによって返された値をパラメータqに代入します。
  • また、オプションの last_query クッキーを str として宣言します。ユーザがクエリqを提供しなかった場合、最後に使用したクエリを使用します。

Dependencies in path operation decorators

  • 場合によっては、パス操作関数の中で依存関係の戻り値を本当に必要としないことがあります。あるいは、依存関係が値を返さない場合もあります。しかし、DBへのCRUDなど、何らかの処理は実行する必要がある時があります。
  • そのような場合は、パス操作関数のパラメータを Depends で宣言する代わりに、依存関係のリストをパス操作デコレータに追加することができます。

Add dependencies to the path operation decorator

  • パス操作デコレータは、オプションの引数 dependencies を受け取ります。
  • それは Depends() のリストでなければなりません。
from fastapi import Depends, FastAPI, Header, HTTPException

app = FastAPI()


async def verify_token(x_token: str = Header(...)):
    if x_token != "fake-super-secret-token":
        raise HTTPException(status_code=400, detail="X-Token header invalid")


async def verify_key(x_key: str = Header(...)):
    if x_key != "fake-super-secret-key":
        raise HTTPException(status_code=400, detail="X-Key header invalid")
    return x_key


@app.get("/items/", dependencies=[Depends(verify_token), Depends(verify_key)])
async def read_items():
    return [{"item": "Foo"}, {"item": "Bar"}]
  • これらの依存関係は、通常の依存関係と同じように実行/解決されます。しかし、それらの値 (何かを返す場合) はパス操作関数には渡されません。
  • エディタによっては、未使用の関数パラメータをチェックしてエラーとして表示するものもあります。
  • これらの依存関係をパス操作デコレータで使用することで、エディタやツールのエラーを回避しながら、それらが実行されていることを確認することができます。
  • また、新しい開発者がコード中の未使用のパラメータを見て、それが不要だと思ってしまうという混乱を避けるのにも役立つかもしれません。
  • この例では、X-Key と X-Token という独自のカスタムヘッダを使用しています。
  • しかし、実際のケースでは、セキュリティを実装する際には、統合されたセキュリティユーティリティ(次の章で説明します)を使用することで、より多くの利点を得ることができます。
  • これらはリクエスト要件 (ヘッダのようなもの) やその他のサブ依存関係を宣言することができます。
  • これらの依存関係は、通常の依存関係と同じように例外を発生させることができます。

Return values

  • そして、値を返すかどうかは別として、値が使われることはありません。つまり、すでにどこかで使っている普通の依存関係(値を返すもの)を再利用することができ、値は使われなくても依存関係は実行されます。

Dependencies for a group of path operations

  • 後日、大規模なアプリケーションを構造化する方法 (Bigger Applications - Multiple Files) を読むときに、おそらく複数のファイルを使用して、パス操作のグループに対して単一の依存性パラメータを宣言する方法を学びます。

Global Dependencies

  • アプリケーションの種類によっては、アプリケーション全体に依存関係を追加したい場合があります。
  • パス操作デコレータに依存性を追加する方法と同様に、FastAPI アプリケーションに依存性を追加することができます。その場合、アプリケーション内のすべてのパス操作に適用されます。
  • そして、パス操作デコレータに依存性を追加することについてのセクションのアイデアはすべて、まだ適用されますが、この場合は、アプリ内のすべてのパス操作に適用されます。

Dependencies with yield

  • FastAPIは、終了後に追加処理を行う依存関係をサポートしています。returnの代わりにyieldを使用し、追加処理を後に書きます。

A database dependency with yield

  • 例えば、これを使ってデータベースセッションを作成し、終了後にそれを閉じることができます。yield文を含む前のコードのみが、レスポンスを送信する前に実行されます。
async def get_db():
    db = DBSession()
    try:
        yield db
    finally:
        db.close()
  • yieldされた値は、パス操作や他の依存関係に注入される
  • レスポンスが実行された後にfinally以降の処理が実行される

A dependency with yield and try

  • yield を持つ依存関係で try ブロックを使用すると、その依存関係を使用する際にスローされた例外を受け取ることになります。
  • 例えば、他の依存関係やパス操作の中で、途中のあるコードがデータベーストランザクションを "ロールバック "したり、その他のエラーを発生させたりした場合、依存関係の中で例外を受け取ることになります。
  • そこで、someExceptionをexceptして依存関係の中でその特定の例外を探すことができます。
  • 同様に、例外があったかどうかに関わらず、終了ステップが実行されているかどうかを確認するために finally を使用することができます。

Sub-dependencies with yield

  • 任意のサイズと形状のサブ依存関係とサブ依存関係の「ツリー」を持つことができ、そのうちのどれかまたはすべてで yield を使用することができます。
  • FastAPIは、yieldを使用した各依存関係の「終了コード」が正しい順番で実行されるようにします。
  • 例えば、dependency_cはdependency_bに依存関係を持ち、dependency_bはdependency_aに依存関係を持つことができます。
  • そして、それらはすべて yield を使用することができます。
  • この場合、 dependency_c は、その終了コードを実行するために dependency_b (ここでは dep_b という名前) の値がまだ利用可能である必要があります。
  • また、dependency_b は、その終了コードを実行するために dependency_a (ここでは dep_a という名前) の値が利用可能である必要があります。
  • 同じように、yieldとreturnが混在した依存関係を持つことができます。
  • また、1つの依存関係を持つことで、他の依存関係をいくつか必要とし、その依存関係を yield などとすることもできます。
  • 必要な依存関係の任意の組み合わせを持つことができます。FastAPIは、すべてが正しい順序で実行されるようにします。

Dependencies with yield and HTTPException

  • yield で依存関係を使用し、例外をキャッチする try ブロックを持つことができることがわかりました。yield の後の終了コードで HTTPException のようなものを発生させたくなるかもしれません。しかし、それはうまくいきません。
  • yield を持つ依存関係の終了コードは、例外ハンドラの後に実行されます。依存関係で投げられた例外を出口コード(yieldの後)でキャッチすることは何もありません。
  • つまり、yield の後に HTTPException を発生させた場合、HTTPException をキャッチして HTTP 400 レスポンスを返すデフォルトの (あるいは任意のカスタムの) 例外ハンドラは、その例外をキャッチするために存在しなくなります。
  • これにより、依存関係で設定されたもの (例えば DB セッション) を、例えばバックグラウンドタスクで使用することが可能になります。
  • バックグラウンドタスクはレスポンスが送信された後に実行されます。そのため、すでに送信されたレスポンスを変更する方法すらないため、HTTPExceptionを発生させる方法がありません。
  • しかし、バックグラウンドタスクがDBエラーを発生させた場合、少なくともyieldを使って依存関係のセッションをロールバックしたり、きれいに閉じたりすることができ、エラーをログに記録したり、リモートのトラッキングシステムに報告したりすることができます。
  • 例外が発生する可能性があるコードがある場合は、最も一般的で "Pythonic "なことをして、コードのその部分にtryブロックを追加してください。
  • レスポンスを返す前に処理したいカスタム例外がある場合や、 レスポンスを変更したい場合、HTTPException を発生させたい場合は、カスタム例外ハンドラを作成します。

Context Managers

  • "コンテキストマネージャ "とは、with文の中で使用できるPythonオブジェクトのことです。例えば、ファイルを読むために with を使うことができます
with open("./somefile.txt") as f:
    contents = f.read()
    print(contents)
  • その下で、open("./somefile.txt")は「コンテキストマネージャ」と呼ばれるオブジェクトを作成します。
  • withブロックが終了すると、例外があっても必ずファイルを閉じるようにします。
  • yield で依存関係を作成すると、FastAPI は内部的にそれをコンテキストマネージャーに変換し、他の関連ツールと組み合わせます。

Using context managers in dependencies with yield

後日加筆修正します

4
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?