大規模アプリケーション - 複数ファイル¶
アプリケーションまたは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
クラスと同様に、インポートして「インスタンス」を作成します。
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
クラスを使用する場合と同じ方法で使用します。
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
ヘッダーを読み取るための単純な依存関係を使用します。
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")
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
バージョンを使用することをお勧めします。
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
に追加できます。
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
に依存関係
を含めることで、たとえば、パスオペレーションのグループ全体に対する認証を要求できます。個別に各パスオペレーションに追加されていない場合でも。
確認してください
prefix
、tags
、responses
、dependencies
パラメーターは(多くの場合と同様に)、コードの重複を回避するための**FastAPI**の機能です。
依存関係のインポート¶
このコードは、app.routers.items
モジュール、app/routers/items.py
ファイルに存在します。
そして、app.dependencies
モジュール(app/dependencies.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
に追加したため、各パスオペレーションにプリフィックス/items
やtags=["items"]
を追加していません。
しかし、特定のパスオペレーションに適用される追加のタグ
と、そのパスオペレーション固有の追加のレスポンス
を追加できます。
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
の依存関係と組み合わせられるグローバル依存関係を宣言することもできます。
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
を持つ他のサブモジュールをインポートします。
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
という名前の別の変数があるためです。
次のように、1つずつインポートした場合、
from .routers.items import router
from .routers.users import router
users
のrouter
はitems
のrouter
を上書きし、同時にそれらを使用できなくなります。
そのため、同じファイルで両方を使用できるように、サブモジュールを直接インポートします。
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
とitems
のAPIRouter
を含める¶
次に、users
とitems
サブモジュールのrouter
を含めます。
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
で宣言された各パスオペレーションに対して、パスオペレーションが実際に作成されます。
つまり、裏側では、すべてが同じ単一アプリであるかのように動作します。
確認してください
ルーターを含める際の性能については心配する必要はありません。
これはマイクロ秒単位で行われ、起動時のみ発生します。
そのため、パフォーマンスに影響を与えることはありません。⚡
カスタムprefix
、tags
、responses
、およびdependencies
を持つAPIRouter
を含める¶
さて、あなたの組織がapp/internal/admin.py
ファイルを提供してくれたと想像してみましょう。
このファイルには、組織が複数のプロジェクト間で共有するいくつかの管理者用パスオペレーションを含むAPIRouter
が含まれています。
この例では非常にシンプルですが、組織内の他のプロジェクトと共有されているため、prefix
、dependencies
、tags
などをAPIRouter
に直接追加して修正することはできないとしましょう。
from fastapi import APIRouter
router = APIRouter()
@router.post("/")
async def update_admin():
return {"message": "Admin getting schwifty"}
しかし、それでも、すべてのパスオペレーションが/admin
で始まるように、APIRouter
を含める際にカスタムprefix
を設定したいと考えています。また、このプロジェクトで既に持っているdependencies
でセキュリティ保護し、tags
とresponses
を含めたいと考えています。
これらのパラメーターをapp.include_router()
に渡すことで、元のAPIRouter
を変更することなく、それらを宣言できます。
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
アプリに直接追加することもできます。
ここでは、それが可能であることを示すためにそうしています 🤷
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
を含める¶
FastAPI
アプリケーションにAPIRouter
を含めるのと同じ方法で、次を使用して別のAPIRouter
にAPIRouter
を含めることができます。
router.include_router(other_router)
other_router
のパスオペレーションも含まれるように、router
をFastAPI
アプリに含める前にこれを実行してください。