Skip to content

Settings

In every application, there arises a need for project-specific settings to ensure its uniqueness.

As a project grows in complexity, and settings become dispersed throughout the codebase, managing them can become challenging, leading to a potential organizational mess.

Lilya leverages Dymmond Setings.

Warning

All the settings in Lilya use Python dataclasses.

How to use

There are two ways of using the settings object within a Lilya application.

  • Using the LILYA_SETTINGS_MODULE environment variable
  • Using the settings_module instance attribute.

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

Settings and the application

When starting a Lilya instance if no parameters are provided, it will automatically load the defaults from the system settings object, the Settings.

from lilya.apps import Lilya

# Loads application default values from Settings
app = Lilya()
from lilya.apps import Lilya

# Creates the application instance with app_name and version set
# and loads the remaining parameters from the Settings
app = Lilya(
    debug=True,
    middleware=...,
    permissions=...,
)

Custom settings

Using the defaults from Settings 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 Settings.

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

from __future__ import annotations

from dataclasses import dataclass

from lilya.conf import Settings
from lilya.middleware import DefineMiddleware
from lilya.middleware.httpsredirect import HTTPSRedirectMiddleware


@dataclass
class AppSettings(Settings):
    # The default is already production but for this example
    # we set again the variable
    environment: bool = "production"
    debug: bool = False
    reload: bool = False

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

import logging
import sys
from dataclasses import dataclass
from typing import Any

from loguru import logger

from lilya.logging import InterceptHandler
from lilya.types import LifespanEvent

from ..configs.base import AppSettings


async def start_database(): ...


async def close_database(): ...


@dataclass
class DevelopmentSettings(AppSettings):
    # the environment can be names to whatever you want.
    environment: bool = "develpoment"
    debug: bool = True
    reload: bool = True

    def __init__(self, *args: Any, **kwds: Any) -> Any:
        super().__init__(*args, **kwds)
        logging_level = logging.DEBUG if self.debug else logging.INFO
        loggers = ("uvicorn.asgi", "uvicorn.access", "lilya")
        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[LifespanEvent]:
        """
        List of events/actions to be done on_startup.
        """
        return [start_database]

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

from dataclasses import dataclass

from lilya.types import LifespanEvent

from ..configs.base import AppSettings


async def start_database(): ...


async def close_database(): ...


@dataclass
class TestingSettings(AppSettings):
    # the environment can be names to whatever you want.
    environment: bool = "testing"
    debug: bool = True
    reload: bool = False

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

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

from dataclasses import dataclass

from lilya.types import LifespanEvent

from ..configs.base import AppSettings


async def start_database(): ...


async def close_database(): ...


@dataclass
class ProductionSettings(AppSettings):
    # the environment can be names to whatever you want.
    environment: bool = "production"
    debug: bool = True
    reload: bool = False

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

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

What just happened?

  1. Created an AppSettings inherited from the Settings with common cross environment properties.
  2. Created one settings file per environment and inherited from the base AppSettings.
  3. Created specific events on_startup and on_shutdown for each environment.

Settings Module

Lilya by default is looking for a LILYA_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.
LILYA_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, LILYA_SETTINGS_MODULE looks for the custom settings class created for the application and loads it in lazy mode and make it globaly available.

The settings_module

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

The settings_module is a parameter available in every single Lilya instance as well as ChildLilya.

Creating a settings_module

The configurations have literally the same concept as the Settings, which means that every single settings_module must be derived from the Settings or a FieldException is thrown.

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

from dataclasses import dataclass

from lilya.apps import Lilya
from lilya.conf import Settings


@dataclass
class LilyaSettings(Settings):
    debug: bool = False
    secret_key: str = "a child secret"


app = Lilya(
    routes=...,
    settings_module=LilyaSettings,
)

Is this simple, literally, Lilya 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.

Settings module as a string

Lilya also offers the possibility of importing the settings via string. This means that you can literally make you codebase cleaner and avoid imports everywhere by simply using the dotted path import.

Example

Let us assume we have a settings file with a class name AppSettings located inside myapp/configs/settings.py.

How to import via string?

from lilya.apps import Lilya
from lilya.routing import Path


async def home(): ...


app = Lilya(
    routes=[Path("/", handler=home)],
    settings_module="myapp.configs.settings.AppSettings",
)

Order of priority

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

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

  • A Lilya application with normal settings.
  • A ChildLilya with a specific set of configurations unique to it.
from dataclasses import dataclass

from lilya.apps import ChildLilya, Lilya
from lilya.conf import Settings
from lilya.routing import Include


@dataclass
class ChildLilyaSettings(Settings):
    debug: bool = True
    secret_key: str = "a child secret"


## Create a ChildLilya application
child_app = ChildLilya(
    routes=[...],
    settings_module=ChildLilyaSettings,
)

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

What is happenening here?

In the example above we:

  • Created a settings object derived from the main Settings and passed some defaults.
  • Passed the ChildLilyaSettings into the ChildLilya instance.
  • Passed the ChildLilya into the Lilya 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 the settings_module.
  • If settings_module does not provide an app_name value, it will look for the value in the LILYA_SETTINGS_MODULE.
  • If no LILYA_SETTINGS_MODULE environment variable is provided by you, then it will default to the Lilya defaults. Read more about this here.

So the order of priority:

  • Parameter instance value takes priority above settings_module.
  • settings_module takes priority above LILYA_SETTINGS_MODULE.
  • LILYA_SETTINGS_MODULE is the last being checked.

Settings config and Lilya 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 Lilya settings object to be used by the LILYA_SETTINGS_MODULE.
  2. We create a settings_module to be used by the Lilya 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 LILYA_SETTINGS_MODULE

src/configs/main_settings.py
from dataclasses import dataclass
from typing import List

from lilya.conf import Settings
from lilya.exceptions import PermissionDenied
from lilya.permissions import DefinePermission
from lilya.protocols.permissions import PermissionProtocol
from lilya.requests import Request
from lilya.types import ASGIApp, Receive, Scope, Send


class AllowAccess(PermissionProtocol):
    def __init__(self, app: ASGIApp, *args, **kwargs):
        super().__init__(app, *args, **kwargs)
        self.app = app

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        request = Request(scope=scope, receive=receive, send=send)

        if "allow-admin" in request.headers:
            await self.app(scope, receive, send)
            return
        raise PermissionDenied()


@dataclass
class AppSettings(Settings):
    secret_key: str = "main secret key"

    @property
    def permissions(self) -> List[DefinePermission]:
        return [DefinePermission(AllowAccess)]

Create a configuration to be used by the settings_module

src/configs/app_settings.py
from dataclasses import dataclass

from lilya.conf import Settings


@dataclass
class InstanceSettings(Settings):
    debug: bool = False

Create a Lilya instance

src/app.py
from lilya.apps import Lilya
from lilya.requests import Request
from lilya.responses import JSONResponse
from lilya.routing import Path

from .configs.app_settings import InstanceSettings


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


app = Lilya(
    routes=[Path("/", 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.

LILYA_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 LILYA_SETTINGS_MODULE but we used them at the same time!

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

Parameters

The parameters available inside Settings can be overridden by any custom settings.

Accessing settings

To access the application settings there are different ways:

from lilya.apps import Lilya
from lilya.requests import Request
from lilya.routing import Path


async def app_debug(request: Request):
    settings = request.app.settings
    return {"debug": settings.debug}


app = Lilya(routes=[Path("/", app_debug)])
from lilya.apps import Lilya
from lilya.conf import settings
from lilya.routing import Path


async def app_debug():
    return {"debug": settings.debug}


app = Lilya(routes=[Path("/", app_debug)])

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 dataclasses import dataclass

from lilya.conf import Settings
from lilya.middleware.httpsredirect import HTTPSRedirectMiddleware
from lilya.middleware import DefineMiddleware


@dataclass
class AppSettings(Settings):
    debug: bool = False

    @property
    def middleware(self) -> List[DefineMiddleware]:
        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 lilya.apps import Lilya

app = Lilya(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 a Lilya 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 it is because will make your codebase more organised and easier to maintain.

Check

When you pass the parameters via instantiation of a Lilya 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.debug directly.