Конфиги используются в каждом приложении. Многие разработчики используют для управления конфигурационными файлами стандартные библиотеки по типу json и yaml, а также python-dotenv для загрузки чувствительных данных из файла в переменные окружения. В этой статье мы научимся загружать как нечувствительные данные из файлов TOML, так и переменные из .env в классы
Подготовка
Установим нужные библиотеки в окружение:
pip install pydantic-settings
Затем в корне проекта создадим:
-
Файл
main.py -
Директорию
settings, которая будет содержать 2 файла:config.tomlи.env -
Директорию
config, которая непосредственно будет содержать код для управления конфигами. Внутри нее создадимmain.pyи__init__.py
Получаем следующую структуру:
│ main.py │ ├───config │ │ main.py │ │ __init__.py │ ├───settings .env config.toml
Наполнение файла settings/.env :
db_host = "localhost" db_password = "db-password-123" tg_bot_token = "bot-token-secret" tg_api_id = "api-id-secret"
settings/config.toml :
[fastapi] host = "127.0.0.1" port = 6000 [bot] admin_id = "@admin_username" [redis] host = "127.0.0.1" port = 6739
Обрабатываем переменные окружения
Для того, чтобы не тратить время, напишем в config/__init__.py :
from .main import *
Теперь переходим к config/main.py. Для начала импортируем нужные библиотеки
from pydantic_settings import BaseSettings, SettingsConfigDict
Теперь создадим базовые настройки для конфига
class ConfigBase(BaseSettings): model_config = SettingsConfigDict( env_file="settings/.env", env_file_encoding="utf-8", extra="ignore" )
Рассмотрим код выше
Класс BaseSettings предоставляет функционал для чтения переменных окружения в атрибуты класса. Поле model_config позволяет нам настроить класс, чтобы не дублировать его при объявлении каждого атрибута. Класс SettingsConfigDict наследуется от ConfigDict из основного модуля pydantic, который в свою очередь наследуется от TypedDict. Теперь рассмотрим каждый аргумент более подробно:
-
env_fileуказывает путь до файла с переменными окружения -
env_file_encoding— кодировка файла -
extra— принимает строкиallow,ignoreиforbird. При значенииallowмодель разрешает загрузку переменных окружения, которые не соответствуют объявленным требованиям (рассмотрим их в следующем листинге).ignore, в свою очередь, отбрасывает такие переменные, аforbirdзапрещает неподходящие переменные
Теперь начнем объявлять классы конфигов. Начнем с конфигурации для Telegram
class TelegramConfig(ConfigBase): model_config = SettingsConfigDict(env_prefix="tg_") bot_token: str api_id: str
Обратите внимание, что мы наследуемся от созданного нами базового класса. Теперь мы можем не дублировать настройки модели каждый раз. Однако для удобства скажем модели, что нужные нам переменные начинаются с tg_.
В итоге при инициализации класса модель прочитает переменные tg_bot_token и tg_api_id из окружения и запишет их в объявленные поля класса.
Чтобы убедиться, что все работает, в main.py попробуем вызвать наш класс и посмотреть, что у него внутри
from config import TelegramConfig config = TelegramConfig() print(config) # bot_token='bot-token-secret' api_id='api-id-secret'
Все работает так, как и задумано. Теперь по аналогии сделаем классы для всех групп переменных
class DatabaseConfig(ConfigBase): model_config = SettingsConfigDict(env_prefix="db_") host: str password: str
Теперь объединим все конфиги в один класс
class Config(BaseSettings): telegram: TelegramConfig = Field(default_factory=TelegramConfig) db: DatabaseConfig = Field(default_factory=DatabaseConfig)
Аргумент default_factory принимает callable, который будет вызван при инициализации класса.
Наконец, для читаемости создадим классовый метод для загрузки конфига
class Config(BaseSettings): telegram: TelegramConfig = Field(default_factory=TelegramConfig) db: DatabaseConfig = Field(default_factory=DatabaseConfig) @classmethod def load(cls) -> "Config": return cls()
Теперь протестируем то, что у нас получилось
from config import Config config = Config.load() print(f"{config.telegram=}") print(f"{config.db=}") # config.telegram=TelegramConfig(bot_token='bot-token-secret', api_id='api-id-secret') # config.db=DatabaseConfig(host='localhost', password='db-password-123')
Переходим в обработке TOML конфига
Для начала перенесем классы для env переменных в отдельный модуль
config/ │ main.py │ __init__.py │ ├───env_config main.py __init__.py
Теперь config/main.py будет выглядеть следующим образом
from .env_config import TelegramConfig, DatabaseConfig from pydantic_settings import BaseSettings from pydantic import Field class Config(BaseSettings): telegram: TelegramConfig = Field(default_factory=TelegramConfig) db: DatabaseConfig = Field(default_factory=DatabaseConfig) @classmethod def load(cls) -> "Config": return cls()
Далее создадим модуль для обработки TOML конфига, после чего дерево будет выглядеть следующим образом
├───config │ │ main.py │ │ __init__.py │ │ │ ├───env_config │ │ │ main.py │ │ │ __init__.py │ │ │ ├───toml_config │ │ │ main.py │ │ │ __init__.py
pydantic_settings предоставляет нам возможность переопределять источники, из которых модуль загружает переменные. В том числе, поддерживается формат TOML
Для начала в файле config/toml_config/main.py определим модели, которые будут соответствовать каждому блоку из settings/config.toml
from pydantic import BaseModel class RedisConfig(BaseModel): host: str port: int class FastapiConfig(BaseModel): host: str port: int class BotConfig(BaseModel): admin_id: str
На этом наша работа с этим файлом завершена. Теперь возвращаемся в config/main.py
from .env_config import TelegramConfig, DatabaseConfig from .toml_config import RedisConfig, BotConfig, FastapiConfig from pydantic_settings import BaseSettings, TomlConfigSettingsSource from pydantic import Field class Config(BaseSettings): telegram: TelegramConfig = Field(default_factory=TelegramConfig) db: DatabaseConfig = Field(default_factory=DatabaseConfig) fastapi: FastapiConfig redis: RedisConfig bot: BotConfig @classmethod def load(cls) -> "Config": return cls() @classmethod def settings_customise_sources(cls, settings_cls, **kwargs): return (TomlConfigSettingsSource(settings_cls, "settings/config.toml"),)
Здесь видим, что добавился новый классовый метод settings_customise_sources
Он позволяет изменить стандартное поведение класса BaseSettings таким образом, как нам нужно. Подробнее — в документации
Скрываем чувствительные данные в терминале при помощи SecretStr
На данный момент при печати конфига мы можем увидеть все чувствительные данные. Чтобы это исправить, воспользуемся типом SecretStr, который предоставляется модулем pydantic.types
class TelegramConfig(ConfigBase): model_config = SettingsConfigaDict(env_prefix="tg_") bot_token: SecretStr api_id: SecretStr class DatabaseConfig(ConfigBase): model_config = SettingsConfigDict(env_prefix="db_") host: str password: SecretStr
Для того, чтобы получить непосредственно значение, хранимое этим типом, нужно воспользоваться методом get_secret_value() , например:
Config.load().db.password.get_secret_value() #db-password-123
Спасибо @Andrey_Solomatin за это исправление
Итог
Теперь посмотрим, что у нас получилось:
В файле main.py загрузим и распечатаем конфиг
from config import Config config = Config.load() print(config)
Результат:
telegram=TelegramConfig( bot_token=SecretStr('**********'), api_id=SecretStr('**********')) db=DatabaseConfig( host='localhost', password=SecretStr('**********')) fastapi=FastapiConfig( host='127.0.0.1', port=6000) redis=RedisConfig( host='127.0.0.1', port=6739) bot=BotConfig( admin_id='@admin_username' )
ссылка на оригинал статьи https://habr.com/ru/articles/866536/
Добавить комментарий