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

Настройки

С увеличением сложности проекта и распределением настроек по всему коду, начинается беспорядок.

Отличный фреймворк Django предоставляет свой способ управления настройками, но из-за наследия кода и сложности, накопившейся за почти 20 лет разработки, они стали громоздкими и трудными для поддержки.

Вдохновленный Django и опытом 99% разработанных приложений, Esmerald оснащен механизмом для работы с настройками на нативном уровне, используя Pydantic для их обработки.

Note

Начиная с версии 0.8.X, Esmerald позволяет использовать настройки на разных уровнях, делая их полностью модульными.

Способы использования настроек

В приложении Esmerald существует два способа использования объекта настроек:

  • Использование ESMERALD_SETTINGS_MODULE
  • Использование settings_module

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

EsmeraldAPISettings и приложение

При запуске экземпляра Esmerald, если параметры не указаны, автоматически загружаются настройки по умолчанию из системного объекта настроек — EsmeraldAPISettings.

from esmerald import Esmerald

# Loads application default values from EsmeraldAPISettings
app = Esmerald()
from esmerald import Esmerald

# Creates the application instance with app_name and version set
# and loads the remaining parameters from the EsmeraldAPISettings
app = Esmerald(app_name="my app example", version="0.1.0")

Пользовательские настройки

Использование настроек по умолчанию из EsmeraldAPISettings возможно не будет достаточно для большинства приложений.

Поэтому требуются пользовательские настройки.

Все пользовательские настройки должны быть унаследованы от EsmeraldAPISettings.

Предположим, у нас есть три среды для одного приложения: production, testing, development и файл базовых настроек, содержащий общие настройки для всех трех сред.

from __future__ import annotations

from esmerald import EsmeraldAPISettings
from esmerald.conf.enums import EnvironmentType
from esmerald.middleware.https import HTTPSRedirectMiddleware
from esmerald.types import Middleware
from lilya.middleware import DefineMiddleware


class AppSettings(EsmeraldAPISettings):
    # The default is already production but for this example
    # we set again the variable
    environment: str = EnvironmentType.PRODUCTION
    debug: bool = False
    reload: bool = False

    @property
    def middleware(self) -> list[Middleware]:
        return [DefineMiddleware(HTTPSRedirectMiddleware)]
from __future__ import annotations

import logging
import sys
from typing import Any

from loguru import logger

from esmerald.conf.enums import EnvironmentType
from esmerald.types import LifeSpanHandler

from ..configs.settings import AppSettings


async def start_database(): ...


async def close_database(): ...


class InterceptHandler(logging.Handler):  # pragma: no cover
    def emit(self, record: logging.LogRecord) -> None:
        level: str
        try:
            level = logger.level(record.levelname).name
        except ValueError:
            level = str(record.levelno)

        frame, depth = logging.currentframe(), 2
        while frame.f_code.co_filename == logging.__file__:
            frame = frame.f_back
            depth += 1

        logger.opt(depth=depth, exception=record.exc_info).log(
            level,
            record.getMessage(),
        )


class DevelopmentSettings(AppSettings):
    # the environment can be names to whatever you want.
    environment: str = EnvironmentType.DEVELOPMENT
    debug: bool = True
    reload: bool = True

    def __init__(self, *args: Any, **kwds: Any):
        super().__init__(*args, **kwds)
        logging_level = logging.DEBUG if self.debug else logging.INFO
        loggers = ("uvicorn.asgi", "uvicorn.access", "esmerald")
        logging.getLogger().handlers = [InterceptHandler()]
        for logger_name in loggers:
            logging_logger = logging.getLogger(logger_name)
            logging_logger.handlers = [InterceptHandler(level=logging_level)]

        logger.configure(handlers=[{"sink": sys.stderr, "level": logging_level}])

    @property
    def on_startup(self) -> list[LifeSpanHandler]:
        """
        List of events/actions to be done on_startup.
        """
        return [start_database]

    @property
    def on_shutdown(self) -> list[LifeSpanHandler]:
        """
        List of events/actions to be done on_shutdown.
        """
        return [close_database]
from __future__ import annotations

from esmerald.conf.enums import EnvironmentType
from esmerald.types import LifeSpanHandler

from ..configs.settings import AppSettings


async def start_database(): ...


async def close_database(): ...


class TestingSettings(AppSettings):
    # the environment can be names to whatever you want.
    environment: str = EnvironmentType.TESTING
    debug: bool = True
    reload: bool = False

    @property
    def on_startup(self) -> list[LifeSpanHandler]:
        """
        List of events/actions to be done on_startup.
        """
        return [start_database]

    @property
    def on_shutdown(self) -> list[LifeSpanHandler]:
        """
        List of events/actions to be done on_shutdown.
        """
        return [close_database]
from __future__ import annotations

from esmerald.conf.enums import EnvironmentType
from esmerald.types import LifeSpanHandler

from ..configs.settings import AppSettings


async def start_database(): ...


async def close_database(): ...


class ProductionSettings(AppSettings):
    # the environment can be names to whatever you want.
    environment: str = EnvironmentType.PRODUCTION
    debug: bool = True
    reload: bool = False

    @property
    def on_startup(self) -> list[LifeSpanHandler]:
        """
        List of events/actions to be done on_startup.
        """
        return [start_database]

    @property
    def on_shutdown(self) -> list[LifeSpanHandler]:
        """
        List of events/actions to be done on_shutdown.
        """
        return [close_database]

Что произошло

  1. Создан AppSettings, унаследованный от EsmeraldAPISettings с общими свойствами для всех сред.
  2. Создан по одному файлу настроек для каждой среды, унаследованных от базового AppSettings.
  3. Импортированы специфические настройки базы данных для каждой среды и добавлены события on_startup и on_shutdown, уникальные для каждой среды.

Модуль настроек Esmerald

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

uvicorn src:app --reload

INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [28720]
INFO:     Started server process [28722]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
ESMERALD_SETTINGS_MODULE=src.configs.production.ProductionSettings uvicorn src:app

INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [28720]
INFO:     Started server process [28722]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
$env:ESMERALD_SETTINGS_MODULE="src.configs.production.ProductionSettings"; uvicorn src:app

INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [28720]
INFO:     Started server process [28722]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

Это очень просто: ESMERALD_SETTINGS_MODULE ищет класс пользовательских настроек, созданный для приложения и загружает его в ленивом режиме.

Модуль настроек (settings_module)

Это отличный инструмент для того, чтобы сделать ваши приложения Esmerald полностью независимыми и модульными. Бывают случаи, когда вам просто нужно подключить существующее приложение Esmerald к другому, а это приложение уже имеет уникальные настройки и значения по умолчанию.

settings_module — это параметр, доступный в каждом экземпляре Esmerald и ChildEsmerald.

Создание settings_module

settings_module имеет абсолютно тот же принцип, что и EsmeraldAPISettings, это означает, что каждый settings_module должен быть унаследован от EsmeraldAPISettings, иначе будет выброшено исключение ImproperlyConfigured.

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

from typing import TYPE_CHECKING

from esmerald import Esmerald, EsmeraldAPISettings
from esmerald.contrib.schedulers.asyncz.config import AsynczConfig

if TYPE_CHECKING:
    from esmerald.types import SchedulerType


# Create a ChildEsmeraldSettings object
class EsmeraldSettings(EsmeraldAPISettings):
    app_name: str = "my application"
    secret_key: str = "a child secret"

    @property
    def scheduler_config(self) -> AsynczConfig:
        return AsynczConfig()


# Create an Esmerald application
app = Esmerald(routes=..., settings_module=EsmeraldSettings)

Esmerald упрощает управление настройками на каждом уровне, сохраняя при этом целостность.

Посмотрите порядок приоритетов, чтобы понять это немного лучше.

Порядок приоритетов

Существует порядок приоритетов, в котором Esmerald считывает ваши настройки.

Если в экземпляр Esmerald передан settings_module, этот объект имеет приоритет над всем остальным.

Предположим следующее:

  • Приложение Esmerald с обычными настройками.
  • ChildEsmerald со специфическим набором конфигураций, уникальных для него.
from esmerald import ChildEsmerald, Esmerald, EsmeraldAPISettings, Include


# Create a ChildEsmeraldSettings object
class ChildEsmeraldSettings(EsmeraldAPISettings):
    app_name: str = "child app"
    secret_key: str = "a child secret"


# Create a ChildEsmerald application
child_app = ChildEsmerald(routes=[...], settings_module=ChildEsmeraldSettings)

# Create an Esmerald application
app = Esmerald(routes=[Include("/child", app=child_app)])

Что здесь происходит

В приведенном выше примере мы:

  1. Создали объект настроек, унаследованный от основного EsmeraldAPISettings и передали некоторые значения по умолчанию.
  2. Передали ChildEsmeraldSettings в экземпляр ChildEsmerald.
  3. Передали ChildEsmerald в приложение Esmerald.

Итак, как осуществляется приоритет с использованием settings_module?

  1. Если значение параметра (при создании экземпляра), например app_name, не указано, будет проверено это же значение внутри settings_module.
  2. Если settings_module не предоставляет значение app_name, оно будет искать значение в ESMERALD_SETTINGS_MODULE.
  3. Если переменная окружения ESMERALD_SETTINGS_MODULE вами не указана, то будет использовано значение по умолчанию Esmerald. Узнайте больше об этом здесь.

Таким образом, порядок приоритетов:

  1. Значение параметра экземпляра имеет приоритет над settings_module.
  2. settings_module имеет приоритет над ESMERALD_SETTINGS_MODULE.
  3. ESMERALD_SETTINGS_MODULE проверяется последним.

Конфигурация настроек и settings_module Esmerald

Красота этого модульного подхода заключается в том, что он позволяет использовать оба подхода одновременно (порядок приоритетов).

Рассмотрим пример, где:

  1. Мы создаем основной объект настроек Esmerald, который будет использоваться ESMERALD_SETTINGS_MODULE.
  2. Мы создаем settings_module, который будет использоваться экземпляром Esmerald.
  3. Мы запускаем приложение, используя оба варианта.

Также предположим, что у вас все настройки находятся в директории src/configs.

Создайте конфигурацию для использования ESMERALD_SETTINGS_MODULE

src/configs/main_settings.py
from typing import TYPE_CHECKING, List

from esmerald import EsmeraldAPISettings
from esmerald.middleware import RequestSettingsMiddleware

if TYPE_CHECKING:
    from esmerald.types import Middleware


# Create a ChildEsmeraldSettings object
class AppSettings(EsmeraldAPISettings):
    app_name: str = "my application"
    secret_key: str = "main secret key"

    @property
    def middleware(self) -> List["Middleware"]:
        return [RequestSettingsMiddleware]

Создайте конфигурацию для использования в setting_config

src/configs/app_settings.py
from esmerald import EsmeraldAPISettings


# Create a ChildEsmeraldSettings object
class InstanceSettings(EsmeraldAPISettings):
    app_name: str = "my instance"

Создайте экземпляр Esmerald

src/app.py
from esmerald import Esmerald, Gateway, JSONResponse, Request, get

from .configs.app_settings import InstanceSettings


@get()
async def home(request: Request) -> JSONResponse: ...


app = Esmerald(routes=[Gateway(handler=home)], settings_module=InstanceSettings)

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

ESMERALD_SETTINGS_MODULE=src.configs.main_settings.AppSettings uvicorn src:app --reload

INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [28720]
INFO:     Started server process [28722]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
$env:ESMERALD_SETTINGS_MODULE="src.configs.main_settings.AppSettings"; uvicorn src:app --reload

INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [28720]
INFO:     Started server process [28722]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

Отлично! Теперь мы использовали settings_module и ESMERALD_SETTINGS_MODULE одновременно!

Посмотрите на порядок приоритетов, чтобы понять, какое значение имеет приоритет и как Esmerald их считывает.

Параметры

Параметры, доступные внутри EsmeraldAPISettings, могут быть переопределены любыми пользовательскими настройками. Подробнее в справочнике по настройкам.

Check

Все конфигурации являются объектами Pydantic. Ознакомьтесь с CORS, CSRF, Session, JWT, StaticFiles, Template и OpenAPI, чтобы узнать, как их использовать.

Примечание: Чтобы понять, какие параметры существуют, а также соответствующие значения, обратитесь к справочнику по настройкам.

Доступ к настройкам

Существует несколько способов доступа к настройкам приложения:

from esmerald import Esmerald, Gateway, Request, get


@get()
async def app_name(request: Request) -> dict:
    settings = request.app.settings
    return {"app_name": settings.app_name}


app = Esmerald(routes=[Gateway(handler=app_name)])
from esmerald import Esmerald, Gateway, get, settings


@get()
async def app_name() -> dict:
    return {"app_name": settings.app_name}


app = Esmerald(routes=[Gateway(handler=app_name)])
from esmerald import Esmerald, Gateway, get
from esmerald.conf import settings


@get()
async def app_name() -> dict:
    return {"app_name": settings.app_name}


app = Esmerald(routes=[Gateway(handler=app_name)])

Info

Некоторая информация могла быть упомянута в других частях документации, но мы предполагаем, что читатели могли её пропустить.

Порядок важности

Использование настроек для запуска приложения вместо предоставления параметров напрямую в момент создания экземпляра не означает, что одно будет работать с другим.

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

Передача параметров в объект всегда будет переопределять значения из настроек по умолчанию.

from esmerald import EsmeraldAPISettings
from esmerald.middleware.https import HTTPSRedirectMiddleware
from esmerald.types import Middleware
from lilya.middleware import DefineMiddleware


class AppSettings(EsmeraldAPISettings):
    debug: bool = False

    @property
    def middleware(self) -> List[Middleware]:
        return [DefineMiddleware(HTTPSRedirectMiddleware)]

Приложение будет:

  1. Запущено с debug как False.
  2. Запущено с промежуточным ПО HTTPSRedirectMiddleware.

Запуск приложения с вышеуказанными настройками обеспечит наличие начального HTTPSRedirectMiddleware и значения debug, но что произойдет, если вы используете настройки вместе с параметрами при создании экземпляра?

from esmerald import Esmerald

app = Esmerald(debug=True, middleware=[])

Приложение будет:

  1. Запущено с debug как True.
  2. Запущено без пользовательских middlewares, если HTTPSRedirectMiddleware был переопределён на [].

Хотя в настройках было указано начать с HTTPSRedirectMiddleware и debug как False, как только вы передаете разные значения при создании объекта Esmerald, эти значения становятся приоритетными.

Объявление параметров в экземпляре всегда будет иметь приоритет над значениями из ваших настроек.

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

Check

Когда вы передаете параметры при создании объекта Esmerald, а не через параметры, при доступе к значениям через request.app.settings эти значения не будут находиться в настройках, так как они были переданы через создание приложения, а не через объект настроек. Доступ к этим значениям можно получить, например, непосредственно через request.app.app_name.