Настройки и переменные окружения¶
Во многих случаях вашему приложению могут понадобиться внешние настройки или конфигурации, например секретные ключи, учетные данные для базы данных, учетные данные для 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 снова и снова для каждого запроса, при этом давая возможность переопределять его во время тестирования.