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.
- Using the ESMERALD_SETTINGS_MODULE
- Using the settings_module
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?
- Created an
AppSettings
inherited from theEsmeraldAPISettings
with common cross environment properties. - Created one settings file per environment and inherited from the base
AppSettings
. - Imported specific database settings per environment and added the events
on_startup
andon_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:
- Created a settings object derived from the main
EsmeraldAPISettings
and passed some defaults. - Passed the
ChildEsmeraldSettings
into theChildEsmerald
instance. - Passed the
ChildEsmerald
into theEsmerald
application.
So, how does the priority take place here using the settings_module
?
- If no parameter value (upon instantiation), for example
app_name
, is provided, it will check for that same value inside thesettings_module
. - If
settings_module
does not provide anapp_name
value, it will look for the value in theESMERALD_SETTINGS_MODULE
. - 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:
- Parameter instance value takes priority above
settings_module
. settings_module
takes priority aboveESMERALD_SETTINGS_MODULE
.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:
- We create a main Esmerald settings object to be used by the
ESMERALD_SETTINGS_MODULE
. - We create a
settings_module
to be used by the Esmerald instance. - 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
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
from esmerald import EsmeraldAPISettings
# Create a ChildEsmeraldSettings object
class InstanceSettings(EsmeraldAPISettings):
app_name: str = "my instance"
Create an Esmerald instance
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:
- Start with
debug
asFalse
. - 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:
- Start with
debug
asTrue
. - 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.