Перейти к содержанию

Настройки и переменные окружения

Во многих случаях вашему приложению могут понадобиться внешние настройки или конфигурации, например секретные ключи, учетные данные для базы данных, учетные данные для email‑сервисов и т.д.

Большинство таких настроек являются изменяемыми (могут меняться), например URL базы данных. И многие из них могут быть «чувствительными», например секреты.

По этой причине обычно их передают через переменные окружения, которые считываются приложением.

Совет

Чтобы понять, что такое переменные окружения, вы можете прочитать Переменные окружения.

Типы и валидация

Переменные окружения могут содержать только текстовые строки, так как они внешние по отношению к Python и должны быть совместимы с другими программами и остальной системой (и даже с разными операционными системами, такими как Linux, Windows, macOS).

Это означает, что любое значение, прочитанное в Python из переменной окружения, будет str, а любые преобразования к другим типам или любая валидация должны выполняться в коде.

Pydantic Settings

К счастью, Pydantic предоставляет отличную утилиту для работы с этими настройками, поступающими из переменных окружения, — Pydantic: управление настройками.

Установка pydantic-settings

Сначала убедитесь, что вы создали виртуальное окружение, активировали его, а затем установили пакет pydantic-settings:

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

Он также включен при установке набора all с:

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

Информация

В Pydantic v1 он входил в основной пакет. Теперь он распространяется как отдельный пакет, чтобы вы могли установить его только при необходимости.

Создание объекта Settings

Импортируйте BaseSettings из Pydantic и создайте подкласс, очень похожий на Pydantic‑модель.

Аналогично Pydantic‑моделям, вы объявляете атрибуты класса с аннотациями типов и, при необходимости, значениями по умолчанию.

Вы можете использовать все те же возможности валидации и инструменты, что и для Pydantic‑моделей, например разные типы данных и дополнительную валидацию через Field().

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 вы бы импортировали BaseSettings напрямую из pydantic, а не из pydantic_settings.

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_user будет int).

Использование 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_EMAIL и APP_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,
    }

В переопределении зависимости мы задаем новое значение admin_email при создании нового объекта Settings, а затем возвращаем этот новый объект.

После этого можно протестировать, что он используется.

Чтение файла .env

Если у вас много настроек, которые могут часто меняться, возможно в разных окружениях, может быть удобно поместить их в файл и читать оттуда как переменные окружения.

Эта практика достаточно распространена и имеет название: такие переменные окружения обычно размещают в файле .env, а сам файл называют «dotenv».

Совет

Файл, начинающийся с точки (.), является скрытым в системах, подобных Unix, таких как Linux и macOS.

Но файл dotenv не обязательно должен иметь именно такое имя.

Pydantic поддерживает чтение таких файлов с помощью внешней библиотеки. Подробнее вы можете прочитать здесь: Pydantic Settings: поддержка Dotenv (.env).

Совет

Чтобы это работало, вам нужно 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.

Здесь мы задаем параметр конфигурации env_file внутри вашего класса Pydantic Settings и устанавливаем значение равным имени файла dotenv, который хотим использовать.

Создание Settings только один раз с помощью lru_cache

Чтение файла с диска обычно затратная (медленная) операция, поэтому, вероятно, вы захотите сделать это один раз и затем переиспользовать один и тот же объект настроек, а не читать файл при каждом запросе.

Но каждый раз, когда мы делаем:

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 — часть functools, что входит в стандартную библиотеку Python. Подробнее можно прочитать в документации Python по @lru_cache.

Итоги

Вы можете использовать Pydantic Settings для управления настройками и конфигурациями вашего приложения с полной мощью Pydantic‑моделей.

  • Используя зависимость, вы упрощаете тестирование.
  • Можно использовать файлы .env.
  • @lru_cache позволяет не читать файл dotenv снова и снова для каждого запроса, при этом давая возможность переопределять его во время тестирования.