コンテンツへスキップ

設定と環境変数

多くの場合、アプリケーションには、シークレットキー、データベース認証情報、メールサービス用の認証情報など、いくつかの外部設定や構成が必要になることがあります。

これらの設定のほとんどは、データベースのURLのように可変です(変更できます)。そして、多くはシークレットのように機密情報である可能性があります。

このため、アプリケーションによって読み取られる環境変数でそれらを提供することが一般的です。

ヒント

環境変数について理解するには、「環境変数」を参照してください。

型と検証

これらの環境変数は、Pythonの外部にあり、他のプログラムやシステムの残りの部分(Linux、Windows、macOSなどの異なるオペレーティングシステムとも)と互換性がある必要があるため、テキスト文字列のみを処理できます。

つまり、環境変数からPythonで読み取られる値はすべてstrであり、異なる型への変換や検証はすべてコードで行う必要があります。

Pydantic Settings

幸いなことに、PydanticはPydantic: Settings managementを使用して、環境変数から来るこれらの設定を処理するための優れたユーティリティを提供しています。

pydantic-settingsをインストール

まず、仮想環境を作成し、それをアクティブにしてから、pydantic-settingsパッケージをインストールしてください。

$ pip install pydantic-settings
---> 100%

all extrasをインストールする際にも含まれています。

$ pip install "fastapi[all]"
---> 100%

情報

Pydantic v1では、メインパッケージに含まれていました。現在では、この機能が必要ない場合にインストールするかどうかを選択できるように、独立したパッケージとして配布されています。

Settingsオブジェクトを作成する

PydanticからBaseSettingsをインポートし、Pydanticモデルと非常によく似たサブクラスを作成します。

Pydanticモデルと同様に、型ヒント付きのクラス属性を宣言し、場合によってはデフォルト値を設定します。

異なるデータ型やField()を使った追加検証など、Pydanticモデルに使用するのと同じ検証機能とツールをすべて使用できます。

from fastapi import FastAPI
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50


settings = Settings()
app = FastAPI()


@app.get("/info")
async def info():
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

情報

Pydantic v1では、BaseSettingspydantic_settingsからではなく、pydanticから直接インポートしていました。

from fastapi import FastAPI
from pydantic import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50


settings = Settings()
app = FastAPI()


@app.get("/info")
async def info():
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

ヒント

すぐにコピー&ペーストできるものが欲しい場合は、この例を使用しないで、以下の最後の例を使用してください。

その後、そのSettingsクラスのインスタンス(この場合はsettingsオブジェクト)を作成すると、Pydanticは環境変数を大文字小文字を区別せずに読み取ります。したがって、大文字の変数APP_NAMEも属性app_nameとして読み取られます。

次に、データを変換して検証します。したがって、そのsettingsオブジェクトを使用すると、宣言した型(例:items_per_userint)のデータが得られます。

settingsを使用する

その後、新しいsettingsオブジェクトをアプリケーションで使用できます。

from fastapi import FastAPI
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50


settings = Settings()
app = FastAPI()


@app.get("/info")
async def info():
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

サーバーを実行する

次に、設定を環境変数として渡してサーバーを実行します。例えば、次のようにADMIN_EMAILAPP_NAMEを設定できます。

$ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" fastapi run main.py

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

ヒント

単一のコマンドに対して複数の環境変数を設定するには、それらをスペースで区切り、すべてコマンドの前に配置します。

そして、admin_email設定は"deadpool@example.com"に設定されます。

app_name"ChimichangApp"になります。

そして、items_per_userはデフォルト値の50を保持します。

別のモジュールでの設定

より大きなアプリケーション - 複数のファイルで見たように、これらの設定を別のモジュールファイルに入れることができます。

たとえば、次のようなconfig.pyファイルがあるかもしれません。

from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50


settings = Settings()

そして、それをmain.pyファイルで使用します。

from fastapi import FastAPI

from .config import settings

app = FastAPI()


@app.get("/info")
async def info():
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

ヒント

より大きなアプリケーション - 複数のファイルで見たように、__init__.pyファイルも必要になります。

依存関係としての設定

場合によっては、どこでも使用されるsettingsを持つグローバルオブジェクトではなく、依存関係から設定を提供することが役立つことがあります。

これは、独自のカスタム設定で依存関係を非常に簡単にオーバーライドできるため、テスト中に特に役立ちます。

設定ファイル

前の例から、config.pyファイルは次のようになるかもしれません。

from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50

今回はデフォルトのインスタンスsettings = Settings()を作成しないことに注意してください。

メインのアプリファイル

ここでは、新しいconfig.Settings()を返す依存関係を作成します。

from functools import lru_cache
from typing import Annotated

from fastapi import Depends, FastAPI

from .config import Settings

app = FastAPI()


@lru_cache
def get_settings():
    return Settings()


@app.get("/info")
async def info(settings: Annotated[Settings, Depends(get_settings)]):
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

ヒント

@lru_cacheについては、後で詳しく説明します。

今のところ、get_settings()は通常の関数であると仮定できます。

そして、それをパス操作関数から依存関係として要求し、必要な場所でどこでも使用できます。

from functools import lru_cache
from typing import Annotated

from fastapi import Depends, FastAPI

from .config import Settings

app = FastAPI()


@lru_cache
def get_settings():
    return Settings()


@app.get("/info")
async def info(settings: Annotated[Settings, Depends(get_settings)]):
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

設定とテスト

その後、get_settingsの依存関係のオーバーライドを作成することで、テスト中に異なる設定オブジェクトを簡単に提供できます。

from fastapi.testclient import TestClient

from .config import Settings
from .main import app, get_settings

client = TestClient(app)


def get_settings_override():
    return Settings(admin_email="testing_admin@example.com")


app.dependency_overrides[get_settings] = get_settings_override


def test_app():
    response = client.get("/info")
    data = response.json()
    assert data == {
        "app_name": "Awesome API",
        "admin_email": "testing_admin@example.com",
        "items_per_user": 50,
    }

依存関係のオーバーライドでは、新しいSettingsオブジェクトを作成するときにadmin_emailに新しい値を設定し、その新しいオブジェクトを返します。

その後、それが使用されていることをテストできます。

.envファイルを読み込む

多くの設定があり、それがさまざまな環境で頻繁に変更される可能性がある場合、それらをファイルに保存し、あたかも環境変数であるかのように読み込むことが役立つかもしれません。

この慣行は十分に一般的で、名前も付いています。これらの環境変数は通常.envファイルに配置され、そのファイルは「dotenv」と呼ばれます。

ヒント

ドット (.) で始まるファイルは、Linux や macOS のような Unix 系システムでは隠しファイルです。

しかし、dotenv ファイルが必ずしもその正確なファイル名である必要はありません。

Pydanticは外部ライブラリを使用してこれらの種類のファイルを読み込むためのサポートを持っています。詳細については、Pydantic Settings: Dotenv (.env) supportを参照してください。

ヒント

これを機能させるには、pip install python-dotenvが必要です。

.envファイル

次のような.envファイルがあるかもしれません。

ADMIN_EMAIL="deadpool@example.com"
APP_NAME="ChimichangApp"

.envから設定を読み込む

そして、config.pyを次のように更新します。

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50

    model_config = SettingsConfigDict(env_file=".env")

ヒント

model_config属性はPydanticの設定にのみ使用されます。Pydantic: Concepts: Configurationで詳細を読むことができます。

from pydantic import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50

    class Config:
        env_file = ".env"

ヒント

ConfigクラスはPydanticの設定にのみ使用されます。Pydantic Model Configで詳細を読むことができます。

情報

Pydanticバージョン1では、設定は内部クラスConfigで行われていましたが、Pydanticバージョン2では、属性model_configで行われます。この属性はdictを受け取り、自動補完とインラインエラーを取得するには、SettingsConfigDictをインポートして使用してそのdictを定義できます。

ここでは、PydanticのSettingsクラス内に設定env_fileを定義し、使用したいdotenvファイルのファイル名に値を設定します。

lru_cacheを使ってSettingsを一度だけ作成する

ディスクからファイルを読み込むことは通常、コストのかかる(遅い)操作であるため、各リクエストごとに読み込むのではなく、一度だけ行って同じ設定オブジェクトを再利用したいと思うでしょう。

しかし、毎回行うたびに、

Settings()

新しいSettingsオブジェクトが作成され、作成時に.envファイルが再度読み込まれます。

依存関係関数が単に次のようであった場合、

def get_settings():
    return Settings()

そのオブジェクトは各リクエストごとに作成され、各リクエストごとに.envファイルが読み込まれることになります。⚠️

しかし、上部に@lru_cacheデコレータを使用しているため、Settingsオブジェクトは最初に呼び出されたときに一度だけ作成されます。✔️

from functools import lru_cache

from fastapi import Depends, FastAPI
from typing_extensions import Annotated

from . import config

app = FastAPI()


@lru_cache
def get_settings():
    return config.Settings()


@app.get("/info")
async def info(settings: Annotated[config.Settings, Depends(get_settings)]):
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

その後のリクエストの依存関係でget_settings()が呼び出されるたびに、get_settings()の内部コードを実行して新しいSettingsオブジェクトを作成する代わりに、最初の呼び出しで返されたのと同じオブジェクトが繰り返し返されます。

lru_cacheの技術的詳細

@lru_cacheは、デコレートされた関数を修正し、関数内のコードを毎回実行して再度計算する代わりに、最初に返されたのと同じ値を返すようにします。

したがって、その下の関数は、引数の組み合わせごとに一度だけ実行されます。そして、それらの引数の組み合わせのそれぞれによって返された値は、関数がまったく同じ引数の組み合わせで呼び出されるたびに繰り返し使用されます。

たとえば、次のような関数がある場合、

@lru_cache
def say_hi(name: str, salutation: str = "Ms."):
    return f"Hello {salutation} {name}"

プログラムは次のように実行される可能性があります。

sequenceDiagram

participant code as Code
participant function as say_hi()
participant execute as Execute function

    rect rgba(0, 255, 0, .1)
        code ->> function: say_hi(name="Camila")
        function ->> execute: execute function code
        execute ->> code: return the result
    end

    rect rgba(0, 255, 255, .1)
        code ->> function: say_hi(name="Camila")
        function ->> code: return stored result
    end

    rect rgba(0, 255, 0, .1)
        code ->> function: say_hi(name="Rick")
        function ->> execute: execute function code
        execute ->> code: return the result
    end

    rect rgba(0, 255, 0, .1)
        code ->> function: say_hi(name="Rick", salutation="Mr.")
        function ->> execute: execute function code
        execute ->> code: return the result
    end

    rect rgba(0, 255, 255, .1)
        code ->> function: say_hi(name="Rick")
        function ->> code: return stored result
    end

    rect rgba(0, 255, 255, .1)
        code ->> function: say_hi(name="Camila")
        function ->> code: return stored result
    end

私たちの依存関係であるget_settings()の場合、この関数は引数を一切取らないため、常に同じ値を返します。

これにより、まるでグローバル変数であるかのように振る舞います。しかし、依存関係関数を使用しているため、テスト時に簡単にオーバーライドできます。

@lru_cacheはPythonの標準ライブラリの一部であるfunctoolsに属しており、Pythonの@lru_cacheに関するドキュメントで詳細を確認できます。

まとめ

Pydantic Settingsを使用して、Pydanticモデルのすべての機能を使って、アプリケーションの設定や構成を処理できます。

  • 依存関係を使用することで、テストを簡素化できます。
  • .envファイルを一緒に使用できます。
  • @lru_cacheを使用すると、各リクエストごとにdotenvファイルを何度も読み込むことを回避でき、テスト中にそれをオーバーライドすることもできます。