コンテンツへスキップ

大規模なアプリケーション - 複数ファイル

アプリケーションやWeb APIを構築する場合、すべてを単一のファイルにまとめることができるのは稀です。

FastAPIは、すべての柔軟性を保ちながら、アプリケーションを構造化するための便利なツールを提供します。

情報

Flaskを使ったことがあるなら、これはFlaskのBlueprintに相当するものです。

ファイル構造の例

次のようなファイル構造があるとしましょう

.
├── app
│   ├── __init__.py
│   ├── main.py
│   ├── dependencies.py
│   └── routers
│   │   ├── __init__.py
│   │   ├── items.py
│   │   └── users.py
│   └── internal
│       ├── __init__.py
│       └── admin.py

ヒント

いくつかの`__init__.py`ファイルがあります:各ディレクトリまたはサブディレクトリに1つずつ。

これにより、あるファイルから別のファイルへコードをインポートできるようになります。

例えば、`app/main.py`には次のような行があるかもしれません

from app.routers import items
  • appディレクトリはすべてを含みます。そして、空のファイルapp/__init__.pyがあるので、これは「Pythonパッケージ」(「Pythonモジュール」の集合)です:app
  • app/main.pyファイルが含まれています。これはPythonパッケージ(ファイル__init__.pyを持つディレクトリ)の中にあるので、そのパッケージの「モジュール」です:app.main
  • また、`app/dependencies.py`ファイルもあり、`app/main.py`と同様に、「モジュール」です:`app.dependencies`。
  • 別のファイル`__init__.py`を持つサブディレクトリ`app/routers/`があり、これは「Pythonサブパッケージ」です:`app.routers`。
  • ファイル`app/routers/items.py`はパッケージ`app/routers/`の中にあるので、サブモジュールです:`app.routers.items`。
  • app/routers/users.pyも同様で、別のサブモジュールです:app.routers.users
  • また、別のファイル`__init__.py`を持つサブディレクトリ`app/internal/`もあり、これは別の「Pythonサブパッケージ」です:`app.internal`。
  • そして、ファイル`app/internal/admin.py`は別のサブモジュールです:`app.internal.admin`。

コメント付きの同じファイル構造

.
├── app                  # "app" is a Python package
│   ├── __init__.py      # this file makes "app" a "Python package"
│   ├── main.py          # "main" module, e.g. import app.main
│   ├── dependencies.py  # "dependencies" module, e.g. import app.dependencies
│   └── routers          # "routers" is a "Python subpackage"
│   │   ├── __init__.py  # makes "routers" a "Python subpackage"
│   │   ├── items.py     # "items" submodule, e.g. import app.routers.items
│   │   └── users.py     # "users" submodule, e.g. import app.routers.users
│   └── internal         # "internal" is a "Python subpackage"
│       ├── __init__.py  # makes "internal" a "Python subpackage"
│       └── admin.py     # "admin" submodule, e.g. import app.internal.admin

APIRouter

ユーザーのみを扱う専用のファイルが`/app/routers/users.py`にあるサブモジュールだとしましょう。

ユーザーに関連する*パスオペレーション*をコードの他の部分から分離して、整理しておきたいとします。

しかし、それはまだ同じ**FastAPI**アプリケーション/Web APIの一部です(同じ「Pythonパッケージ」の一部です)。

そのモジュールの*パスオペレーション*を`APIRouter`を使って作成できます。

APIRouterのインポート

FastAPIクラスと同じようにインポートして「インスタンス」を作成します

app/routers/users.py
from fastapi import APIRouter

router = APIRouter()


@router.get("/users/", tags=["users"])
async def read_users():
    return [{"username": "Rick"}, {"username": "Morty"}]


@router.get("/users/me", tags=["users"])
async def read_user_me():
    return {"username": "fakecurrentuser"}


@router.get("/users/{username}", tags=["users"])
async def read_user(username: str):
    return {"username": username}

APIRouterによる*パスオペレーション*

そして、それを使って*パスオペレーション*を宣言します。

FastAPIクラスと同じように使います

app/routers/users.py
from fastapi import APIRouter

router = APIRouter()


@router.get("/users/", tags=["users"])
async def read_users():
    return [{"username": "Rick"}, {"username": "Morty"}]


@router.get("/users/me", tags=["users"])
async def read_user_me():
    return {"username": "fakecurrentuser"}


@router.get("/users/{username}", tags=["users"])
async def read_user(username: str):
    return {"username": username}

APIRouterを「ミニ`FastAPI`」クラスと考えることができます。

すべての同じオプションがサポートされています。

すべての同じ`parameters`、`responses`、`dependencies`、`tags`など。

ヒント

この例では、変数は`router`と呼ばれていますが、好きなように名前を付けることができます。

この`APIRouter`をメインの`FastAPI`アプリに含めますが、その前に、依存関係と別の`APIRouter`を確認しましょう。

依存性注入

アプリケーションの複数の場所で使用されるいくつかの依存関係が必要になることがわかります。

そこで、それらを独自の`dependencies`モジュール(`app/dependencies.py`)に置きます。

ここでは、カスタムの`X-Token`ヘッダーを読み取るための単純な依存関係を使用します

app/dependencies.py
from typing import Annotated

from fastapi import Header, HTTPException


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


async def get_query_token(token: str):
    if token != "jessica":
        raise HTTPException(status_code=400, detail="No Jessica token provided")
app/dependencies.py
from fastapi import Header, HTTPException
from typing_extensions import Annotated


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


async def get_query_token(token: str):
    if token != "jessica":
        raise HTTPException(status_code=400, detail="No Jessica token provided")

ヒント

可能であれば`Annotated`バージョンを使用することをお勧めします。

app/dependencies.py
from fastapi import Header, HTTPException


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


async def get_query_token(token: str):
    if token != "jessica":
        raise HTTPException(status_code=400, detail="No Jessica token provided")

ヒント

この例を簡単にするために、架空のヘッダーを使用しています。

しかし、実際の場合には、統合されたセキュリティユーティリティを使用する方が良い結果が得られます。

APIRouterを持つ別のモジュール

また、アプリケーションの「アイテム」を扱う専用のエンドポイントが、`app/routers/items.py`にあるモジュールにあるとします。

以下のための*パスオペレーション*があります

  • /items/
  • /items/{item_id}

`app/routers/users.py`と同じ構造です。

しかし、もっと賢くコードを少し単純化したいと考えています。

このモジュール内のすべての*パスオペレーション*が同じものを持っていることを知っています

  • パスの`prefix`:`/items`。
  • tags:(タグは1つだけ:`items`)。
  • 追加の`responses`。
  • dependencies:それらはすべて、私たちが作成した`X-Token`の依存関係を必要とします。

そこで、それらすべてを各*パスオペレーション*に追加する代わりに、`APIRouter`に追加できます。

app/routers/items.py
from fastapi import APIRouter, Depends, HTTPException

from ..dependencies import get_token_header

router = APIRouter(
    prefix="/items",
    tags=["items"],
    dependencies=[Depends(get_token_header)],
    responses={404: {"description": "Not found"}},
)


fake_items_db = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}


@router.get("/")
async def read_items():
    return fake_items_db


@router.get("/{item_id}")
async def read_item(item_id: str):
    if item_id not in fake_items_db:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"name": fake_items_db[item_id]["name"], "item_id": item_id}


@router.put(
    "/{item_id}",
    tags=["custom"],
    responses={403: {"description": "Operation forbidden"}},
)
async def update_item(item_id: str):
    if item_id != "plumbus":
        raise HTTPException(
            status_code=403, detail="You can only update the item: plumbus"
        )
    return {"item_id": item_id, "name": "The great Plumbus"}

各*パスオペレーション*のパスは、次のように`/`で始まる必要があるため

@router.get("/{item_id}")
async def read_item(item_id: str):
    ...

...プレフィックスには最後の`/`を含めてはいけません。

したがって、この場合のプレフィックスは`/items`です。

また、このルーターに含まれるすべての*パスオペレーション*に適用される`tags`のリストと追加の`responses`を追加することもできます。

そして、ルーター内のすべての*パスオペレーション*に追加され、それらへの各リクエストに対して実行/解決される`dependencies`のリストを追加できます。

ヒント

*パスオペレーションデコレータ*の依存関係と同様に、値は*パスオペレーション関数*に渡されないことに注意してください。

最終的な結果として、アイテムのパスは今や次のようになります

  • /items/
  • /items/{item_id}

...意図した通りです。

  • それらは、単一の文字列`"items"`を含むタグのリストでマークされます。
    • これらの「タグ」は、自動インタラクティブドキュメントシステム(OpenAPIを使用)にとって特に便利です。
  • それらはすべて、事前定義された`responses`を含みます。
  • これらの*パスオペレーション*はすべて、それらの前に評価/実行される`dependencies`のリストを持ちます。
    • 特定の*パスオペレーション*で依存関係を宣言した場合、**それらも実行されます**。
    • ルーターの依存関係が最初に実行され、次にデコレータの`dependencies`、そして通常のパラメータの依存関係が実行されます。
    • `scopes`を持つ`Security`依存関係を追加することもできます。

ヒント

APIRouterに`dependencies`を持たせることは、例えば、*パスオペレーション*のグループ全体に対して認証を要求するために使用できます。依存関係がそれぞれに個別に追加されていなくてもです。

確認

`prefix`、`tags`、`responses`、および`dependencies`パラメータは、(他の多くの場合と同様に)コードの重複を避けるための**FastAPI**の機能にすぎません。

依存関係をインポートする

このコードはモジュール`app.routers.items`、ファイル`app/routers/items.py`にあります。

そして、モジュール`app.dependencies`、ファイル`app/dependencies.py`から依存関数を取得する必要があります。

そこで、依存関係には`..`を使った相対インポートを使用します

app/routers/items.py
from fastapi import APIRouter, Depends, HTTPException

from ..dependencies import get_token_header

router = APIRouter(
    prefix="/items",
    tags=["items"],
    dependencies=[Depends(get_token_header)],
    responses={404: {"description": "Not found"}},
)


fake_items_db = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}


@router.get("/")
async def read_items():
    return fake_items_db


@router.get("/{item_id}")
async def read_item(item_id: str):
    if item_id not in fake_items_db:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"name": fake_items_db[item_id]["name"], "item_id": item_id}


@router.put(
    "/{item_id}",
    tags=["custom"],
    responses={403: {"description": "Operation forbidden"}},
)
async def update_item(item_id: str):
    if item_id != "plumbus":
        raise HTTPException(
            status_code=403, detail="You can only update the item: plumbus"
        )
    return {"item_id": item_id, "name": "The great Plumbus"}

相対インポートの仕組み

ヒント

インポートの仕組みを完全に理解している場合は、下の次のセクションに進んでください。

シングルドット `.` は、次のように使います

from .dependencies import get_token_header

これは、次を意味します

  • このモジュール(ファイル`app/routers/items.py`)が存在する同じパッケージ(ディレクトリ`app/routers/`)から開始して...
  • モジュール`dependencies`(`app/routers/dependencies.py`にある架空のファイル)を見つけて...
  • そこから関数`get_token_header`をインポートします。

しかし、そのファイルは存在しません。私たちの依存関係は`app/dependencies.py`というファイルにあります。

アプリ/ファイル構造がどのようになっているか思い出してください


2つのドット`..`は、次のような意味です

from ..dependencies import get_token_header

これは、次を意味します

  • このモジュール(ファイル`app/routers/items.py`)が存在する同じパッケージ(ディレクトリ`app/routers/`)から開始して...
  • 親パッケージ(ディレクトリ`app/`)に移動し...
  • そこで、モジュール`dependencies`(`app/dependencies.py`にあるファイル)を見つけてください...
  • そこから関数`get_token_header`をインポートします。

これは正しく動作します!🎉


同様に、3つのドット`...`を使った場合、次のように

from ...dependencies import get_token_header

それは次のことを意味します

  • このモジュール(ファイル`app/routers/items.py`)が存在する同じパッケージ(ディレクトリ`app/routers/`)から開始して...
  • 親パッケージ(ディレクトリ`app/`)に移動し...
  • そのパッケージの親に移動します(親パッケージはありません、`app`がトップレベルです😱)...
  • そこで、モジュール`dependencies`(`app/dependencies.py`にあるファイル)を見つけてください...
  • そこから関数`get_token_header`をインポートします。

それは`app/`より上の、独自の`__init__.py`ファイルなどを持つ何らかのパッケージを参照することになります。しかし、我々にはそれがありません。したがって、それは我々の例ではエラーを投げます。🚨

しかし、今ではその仕組みがわかったので、どんなに複雑なアプリでも相対インポートを使用できます。🤓

カスタムのtagsresponsesdependenciesを追加する

プレフィックス`/items`や`tags=["items"]`を各*パスオペレーション*に追加していません。なぜなら、それらを`APIRouter`に追加したからです。

しかし、特定の*パスオペレーション*に適用される*さらなる* `tags`を追加したり、その*パスオペレーション*に特有の追加の`responses`を追加することもできます

app/routers/items.py
from fastapi import APIRouter, Depends, HTTPException

from ..dependencies import get_token_header

router = APIRouter(
    prefix="/items",
    tags=["items"],
    dependencies=[Depends(get_token_header)],
    responses={404: {"description": "Not found"}},
)


fake_items_db = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}


@router.get("/")
async def read_items():
    return fake_items_db


@router.get("/{item_id}")
async def read_item(item_id: str):
    if item_id not in fake_items_db:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"name": fake_items_db[item_id]["name"], "item_id": item_id}


@router.put(
    "/{item_id}",
    tags=["custom"],
    responses={403: {"description": "Operation forbidden"}},
)
async def update_item(item_id: str):
    if item_id != "plumbus":
        raise HTTPException(
            status_code=403, detail="You can only update the item: plumbus"
        )
    return {"item_id": item_id, "name": "The great Plumbus"}

ヒント

この最後のパスオペレーションは、`["items", "custom"]`というタグの組み合わせを持つことになります。

そして、ドキュメントには`404`と`403`の両方のレスポンスが表示されます。

メインのFastAPI

さて、`app/main.py`にあるモジュールを見てみましょう。

ここで`FastAPI`クラスをインポートして使用します。

これが、アプリケーションのすべてをまとめるメインファイルになります。

そして、ロジックのほとんどが独自の特定のモジュールに存在するようになるため、メインファイルは非常にシンプルになります。

FastAPIのインポート

通常通り`FastAPI`クラスをインポートして作成します。

そして、各`APIRouter`の依存関係と結合されるグローバルな依存関係を宣言することもできます

app/main.py
from fastapi import Depends, FastAPI

from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])


app.include_router(users.router)
app.include_router(items.router)
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],
    responses={418: {"description": "I'm a teapot"}},
)


@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}

APIRouterのインポート

次に、`APIRouter`を持つ他のサブモジュールをインポートします

app/main.py
from fastapi import Depends, FastAPI

from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])


app.include_router(users.router)
app.include_router(items.router)
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],
    responses={418: {"description": "I'm a teapot"}},
)


@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}

ファイル `app/routers/users.py` と `app/routers/items.py` は、同じ Python パッケージ `app` の一部であるサブモジュールなので、シングルドット `.` を使って「相対インポート」でインポートできます。

インポートの仕組み

このセクション

from .routers import items, users

これは、次を意味します

  • このモジュール(ファイル`app/main.py`)が存在する同じパッケージ(ディレクトリ`app/`)から開始して...
  • サブパッケージ`routers`(`app/routers/`にあるディレクトリ)を探して...
  • そこから、サブモジュール`items`(ファイル`app/routers/items.py`)と`users`(ファイル`app/routers/users.py`)をインポートします...

モジュール`items`は変数`router`(`items.router`)を持ちます。これはファイル`app/routers/items.py`で作成したものと同じで、`APIRouter`オブジェクトです。

そして、モジュール`users`に対しても同じことを行います。

次のようにインポートすることもできます

from app.routers import items, users

情報

最初のバージョンは「相対インポート」です

from .routers import items, users

2番目のバージョンは「絶対インポート」です

from app.routers import items, users

Pythonのパッケージとモジュールについてさらに学ぶには、Pythonの公式ドキュメント(モジュールについて)を読んでください。

名前の衝突を避ける

変数`router`だけをインポートするのではなく、サブモジュール`items`を直接インポートしています。

これは、サブモジュール`users`にも`router`という名前の別の変数があるためです。

もし次のように一つずつインポートしていたら

from .routers.items import router
from .routers.users import router

`users`からの`router`が`items`からのものを上書きしてしまい、両方を同時に使うことができなくなります。

そこで、同じファイルで両方を使えるように、サブモジュールを直接インポートします

app/main.py
from fastapi import Depends, FastAPI

from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])


app.include_router(users.router)
app.include_router(items.router)
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],
    responses={418: {"description": "I'm a teapot"}},
)


@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}

usersitemsAPIRouterをインクルード

さて、サブモジュール`users`と`items`から`router`をインクルードしましょう

app/main.py
from fastapi import Depends, FastAPI

from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])


app.include_router(users.router)
app.include_router(items.router)
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],
    responses={418: {"description": "I'm a teapot"}},
)


@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}

情報

`users.router`には、`app/routers/users.py`ファイル内の`APIRouter`が含まれています。

そして、`items.router`には`app/routers/items.py`ファイル内の`APIRouter`が含まれています。

`app.include_router()`を使うと、各`APIRouter`をメインの`FastAPI`アプリケーションに追加できます。

そのルーターのすべてのルートを、その一部として含めます。

技術的な詳細

実際には、`APIRouter`で宣言された各*パスオペレーション*に対して内部的に*パスオペレーション*を作成します。

そのため、舞台裏では、すべてが同じ単一のアプリであるかのように機能します。

確認

ルーターを含める際のパフォーマンスについて心配する必要はありません。

これはマイクロ秒単位で発生し、起動時にのみ行われます。

そのため、パフォーマンスには影響しません。⚡

カスタムのprefixtagsresponsesdependenciesを持つAPIRouterをインクルードする

さて、あなたの組織が`app/internal/admin.py`ファイルをあなたに渡したと想像してください。

これには、組織が複数のプロジェクト間で共有するいくつかの管理者*パスオペレーション*を持つ`APIRouter`が含まれています。

この例では非常にシンプルですが、組織内の他のプロジェクトと共有されているため、`prefix`、`dependencies`、`tags`などを`APIRouter`に直接追加して変更することはできないとしましょう

app/internal/admin.py
from fastapi import APIRouter

router = APIRouter()


@router.post("/")
async def update_admin():
    return {"message": "Admin getting schwifty"}

しかし、`APIRouter`を含めるときにカスタムの`prefix`を設定して、そのすべての*パスオペレーション*が`/admin`で始まるようにしたい、このプロジェクトですでに持っている`dependencies`でそれを保護したい、そして`tags`と`responses`を含めたいと考えています。

これらのパラメータを`app.include_router()`に渡すことで、元の`APIRouter`を変更することなく、すべてを宣言できます

app/main.py
from fastapi import Depends, FastAPI

from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])


app.include_router(users.router)
app.include_router(items.router)
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],
    responses={418: {"description": "I'm a teapot"}},
)


@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}

そうすることで、元の`APIRouter`は変更されないままなので、同じ`app/internal/admin.py`ファイルを組織内の他のプロジェクトと共有し続けることができます。

その結果、私たちのアプリでは、`admin`モジュールからの各*パスオペレーション*は次のようになります

  • プレフィックス `/admin`。
  • タグ `admin`。
  • 依存関係 `get_token_header`。
  • レスポンス `418`。🍵

しかし、それは私たちのアプリのその`APIRouter`にのみ影響し、それを使用する他のコードには影響しません。

そのため、例えば、他のプロジェクトでは同じ`APIRouter`を異なる認証方法で使用することができます。

*パスオペレーション*をインクルード

*パスオペレーション*を`FastAPI`アプリに直接追加することもできます。

ここでは、それができることを示すためだけにそれを行います🤷

app/main.py
from fastapi import Depends, FastAPI

from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])


app.include_router(users.router)
app.include_router(items.router)
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],
    responses={418: {"description": "I'm a teapot"}},
)


@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}

そして、`app.include_router()`で追加された他のすべての*パスオペレーション*と一緒に正しく動作します。

非常に技術的な詳細

:これは非常に技術的な詳細であり、おそらく**単にスキップ**できます。


`APIRouter`は「マウント」されておらず、アプリケーションの他の部分から隔離されていません。

これは、それらの*パスオペレーション*をOpenAPIスキーマとユーザーインターフェースに含めたいためです。

それらを単に隔離して残りの部分とは独立して「マウント」することはできないため、*パスオペレーション*は直接含まれるのではなく、「クローン」されます(再作成されます)。

自動APIドキュメントをチェック

さあ、アプリを実行してください

$ fastapi dev app/main.py

<span style="color: green;">INFO</span>:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

http://127.0.0.1:8000/docsでドキュメントを開いてください。

すべてのサブモジュールからのパスを含む自動APIドキュメントが表示されます。正しいパス(およびプレフィックス)と正しいタグが使用されています

同じルーターを異なる`prefix`で複数回インクルードする

`.include_router()`を*同じ*ルーターで異なるプレフィックスを使って複数回使用することもできます。

これは、例えば、`/api/v1`と`/api/latest`のように、同じAPIを異なるプレフィックスで公開するのに役立つかもしれません。

これは高度な使い方であり、実際には必要ないかもしれませんが、万が一の場合に備えて存在します。

別の`APIRouter`に`APIRouter`をインクルードする

`APIRouter`を`FastAPI`アプリケーションに含めるのと同じように、`APIRouter`を別の`APIRouter`に含めることができます

router.include_router(other_router)

`other_router`の*パスオペレーション*も含まれるように、`FastAPI`アプリに`router`を含める前に必ずこれを行ってください。