コンテンツへスキップ

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

アプリケーションまたは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/main.pyと同じように、app/dependencies.pyファイルもあり、「モジュール」です:app.dependencies
  • app/routers/ サブディレクトリに__init__.pyファイルが存在するため、これは「Python サブパッケージ」です: app.routers
  • app/routers/items.pyファイルはapp/routers/パッケージ内にあるため、これはサブモジュールです: app.routers.items
  • app/routers/users.pyファイルも同様で、別のサブモジュールです: app.routers.users
  • また、app/internal/サブディレクトリに__init__.pyファイルが存在するため、これは別の「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」クラスと考えることができます。

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

すべての同じパラメーターレスポンス依存関係タグなど。

ヒント

この例では、変数は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と同じ構造です。

しかし、もっとスマートになり、コードを少し簡素化したいと考えています。

このモジュールのすべてのパスオペレーションには、次のものがあります。

  • パスプリフィックス: /items
  • タグ:(1つのタグのみ: items)。
  • 追加のレスポンス
  • 依存関係: 作成した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です。

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

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

ヒント

パスオペレーションデコレーターの依存関係と同様に、値はパスオペレーション関数に渡されません。

最終的な結果は、アイテムパスが次のようになることです。

  • /items/
  • /items/{item_id}

…意図したとおりです。

  • それらは、単一の文字列"items"を含むタグのリストでマークされます。
    • これらの「タグ」は、自動対話型ドキュメントシステム(OpenAPIを使用)に特に役立ちます。
  • すべてに事前に定義されたレスポンスが含まれます。
  • これらのすべてのパスオペレーションには、それらを実行する前に依存関係のリストが評価/実行されます。

ヒント

APIRouter依存関係を含めることで、たとえば、パスオペレーションのグループ全体に対する認証を要求できます。個別に各パスオペレーションに追加されていない場合でも。

確認してください

prefixtagsresponsesdependenciesパラメーターは(多くの場合と同様に)、コードの重複を回避するための**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関数をインポートします。

これは、独自の__init__.pyファイルなどを備えた、app/の上位のパッケージを参照します。しかし、それは持っていません。したがって、この例ではエラーが発生します。🚨

しかし、これで仕組みがわかったので、複雑さに関係なく、独自のアプリで相対インポートを使用できます。🤓

カスタムタグレスポンス依存関係の追加

APIRouterに追加したため、各パスオペレーションにプリフィックス/itemstags=["items"]を追加していません。

しかし、特定のパスオペレーションに適用される追加のタグと、そのパスオペレーション固有の追加のレスポンスを追加できます。

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"]があります。

また、ドキュメントには404403の両方のレスポンスが含まれます。

メインの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モジュールには、変数routeritems.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という名前の別の変数があるためです。

次のように、1つずつインポートした場合、

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

usersrouteritemsrouterを上書きし、同時にそれらを使用できなくなります。

そのため、同じファイルで両方を使用できるように、サブモジュールを直接インポートします。

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を含める

次に、usersitemsサブモジュールの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で宣言された各パスオペレーションに対して、パスオペレーションが実際に作成されます。

つまり、裏側では、すべてが同じ単一アプリであるかのように動作します。

確認してください

ルーターを含める際の性能については心配する必要はありません。

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

そのため、パフォーマンスに影響を与えることはありません。⚡

カスタムprefixtagsresponses、およびdependenciesを持つAPIRouterを含める

さて、あなたの組織がapp/internal/admin.pyファイルを提供してくれたと想像してみましょう。

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

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

app/internal/admin.py
from fastapi import APIRouter

router = APIRouter()


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

しかし、それでも、すべてのパスオペレーションが/adminで始まるように、APIRouterを含める際にカスタムprefixを設定したいと考えています。また、このプロジェクトで既に持っているdependenciesでセキュリティ保護し、tagsresponsesを含めたいと考えています。

これらのパラメーターを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を公開する場合に役立ちます。

これは、実際には必要ない可能性のある高度な使用方法ですが、必要になった場合に備えて用意されています。

別のAPIRouterAPIRouterを含める

FastAPIアプリケーションにAPIRouterを含めるのと同じ方法で、次を使用して別のAPIRouterAPIRouterを含めることができます。

router.include_router(other_router)

other_routerのパスオペレーションも含まれるように、routerFastAPIアプリに含める前にこれを実行してください。