Skip to content

Settings

Every application in a way or another needs settings for the uniqueness of the project itself.

When the complexity of a project increases and there are settings spread across the codebase, it is when the things start to get messy.

One great framework, Django, has the settings in place but because of the legacy codebase and the complexity of almost 20 years of development of the framework, those became a bit bloated and hard to maintain.

Inspired by Django and by the experience of 99% of the developed applications using some sort of settings (by environment, by user...), Esmerald comes equipped to handle exactly with that natively and using Pydantic to leverage those.

Note

From the version 0.8.X, Esmerald allows settings on levels making it 100% modular.

The way of the settings

There are two ways of using the settings object within an Esmerald application.

Each one of them has particular use cases but they also work together in perfect harmony.

EsmeraldAPISettings and the application

When starting a Esmerald instance, if no parameters are provided, it will automatically load the defaults from the system settings object, the 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")

Custom settings

Using the defaults from EsmeraldAPISettings generally will not do too much for majority of the applications.

For that reason custom settings are needed.

All the custom settings should be inherited from the EsmeraldAPISettings.

Let's assume we have three environments for one application: production, testing, development and a base settings file that contains common settings across the three environments.

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.logging import InterceptHandler
from esmerald.types import LifeSpanHandler

from ..configs.settings import AppSettings


async def start_database(): ...


async def close_database(): ...


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]

What just happened?

  1. Created an AppSettings inherited from the EsmeraldAPISettings with common cross environment properties.
  2. Created one settings file per environment and inherited from the base AppSettings.
  3. Imported specific database settings per environment and added the events on_startup and on_shutdown specific to each.

Esmerald Settings Module

Esmerald by default is looking for a ESMERALD_SETTINGS_MODULE environment variable to execute any custom settings. If nothing is provided, then it will execute the application defaults.

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.

It is very simple, ESMERALD_SETTINGS_MODULE looks for the custom settings class created for the application and loads it in lazy mode.

The settings_module

This is a great tool to make your Esmerald applications 100% independent and modular. There are cases where you simply want to plug an existing Esmerald application into another and that same Esmerald application already has unique settings and defaults.

The settings_module is a parameter available in every single Esmerald instance as well as ChildEsmerald.

Creating a settings_module

The configurations have literally the same concept as the EsmeraldAPISettings, which means that every single settings_module must be derived from the EsmeraldAPISettings or an ImproperlyConfigured exception is thrown.

The reason why the above is to keep the integrity of the application and settings.

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)

Is this simple, literally, Esmerald simplifies the way you can manipulate settings on each level and keeping the intregrity at the same time.

Check out the order of priority to understand a bit more.

Order of priority

There is an order or priority in which Esmerald reads your settings.

If a settings_module is passed into an Esmerald instance, that same object takes priority above anything else. Let us imagine the following:

  • An Esmerald application with normal settings.
  • A ChildEsmerald with a specific set of configurations unique to it.
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)])

What is happenening here?

In the example above we:

  1. Created a settings object derived from the main EsmeraldAPISettings and passed some defaults.
  2. Passed the ChildEsmeraldSettings into the ChildEsmerald instance.
  3. Passed the ChildEsmerald into the Esmerald application.

So, how does the priority take place here using the settings_module?

  1. If no parameter value (upon instantiation), for example app_name, is provided, it will check for that same value inside the settings_module.
  2. If settings_module does not provide an app_name value, it will look for the value in the ESMERALD_SETTINGS_MODULE.
  3. If no ESMERALD_SETTINGS_MODULE environment variable is provided by you, then it will default to the Esmerald defaults. Read more about this here.

So the order of priority:

  1. Parameter instance value takes priority above settings_module.
  2. settings_module takes priority above ESMERALD_SETTINGS_MODULE.
  3. ESMERALD_SETTINGS_MODULE is the last being checked.

Settings config and Esmerald settings module

The beauty of this modular approach is the fact that makes it possible to use both approaches at the same time (order of priority).

Let us use an example where:

  1. We create a main Esmerald settings object to be used by the ESMERALD_SETTINGS_MODULE.
  2. We create a settings_module to be used by the Esmerald instance.
  3. We start the application using both.

Let us also assume you have all the settings inside a src/configs directory.

Create a configuration to be used by the 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]

Create a configuration to be used by the setting_config

src/configs/app_settings.py
from esmerald import EsmeraldAPISettings


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

Create an Esmerald instance

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)

Now we can start the server using the AppSettings as global and InstanceSettings being passed via instantiation. The AppSettings from the main_settings.py is used to call from the command-line.

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.

Great! Now not only we have used the settings_module and ESMERALD_SETTINGS_MODULE but we used them at the same time!

Check out the order of priority to understand which value takes precedence and how Esmerald reads them out.

Parameters

The parameters available inside EsmeraldAPISettings can be overridden by any custom settings and those are available in the settings reference.

Check

All the configurations are pydantic objects. Check CORS, CSRF, Session, JWT, StaticFiles, Template and OpenAPI and see how to use them.

Note: To understand which parameters exist as well the corresponding values, settings reference is the place to go.

Accessing settings

To access the application settings there are different ways:

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

Some of this information might have been mentioned in some other parts of the documentation but we assume the people reading it might have missed.

Order of importance

Using the settings to start an application instead of providing the parameters directly in the moment of instantiation does not mean that one will work with the other.

When you instantiate an application or you pass parameters directly or you use settings or a mix of both.

Passing parameters in the object will always override the values from the default settings.

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):
    debug: bool = False

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

The application will:

  1. Start with debug as False.
  2. Will start with a middleware HTTPSRedirectMiddleware.

Starting the application with the above settings will make sure that has an initial HTTPSRedirectMiddleware and debug set with values, but what happens if you use the settings + parameters on instantiation?

from esmerald import Esmerald

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

The application will:

  1. Start with debug as True.
  2. Will start without custom middlewares it the HTTPSRedirectMiddleware it was overridden by [].

Although it was set in the settings to start with HTTPSRedirectMiddleware and debug as False, once you pass different values in the moment of instantiating an Esmerald object, those will become the values to be used.

Declaring parameters in the instance will always precede the values from your settings.

The reason why you should be using settings is because it will make your codebase more organised and easier to maintain.

Check

When you pass the parameters via instantiation of an Esmerald object and not via parameters, when accessing the values via request.app.settings, the values won't be in the settings as those were passed via application instantiation and not via settings object. The way to access those values is, for example, request.app.app_name directly.