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.

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.global_settings 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.types import LifespanEvent

from ..configs.base 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__:  # noqa: WPS609
            frame = frame.f_back
            depth += 1

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


@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.global_settings 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.global_settings 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 __future__ import annotations

from dataclasses import dataclass

from lilya.conf.global_settings 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.global_settings 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 following are the available parameters you can configure inside your Settings object. Each one of them has a specific purpose and can be overridden or extended by custom settings.

General

  • debug: bool Whether to display debug tracebacks on server errors. Not recommended in production.

  • environment: str | None Environment name (development, production, testing, etc.). Purely informational.

  • version: str | int | float The version of the application. Defaults to Lilya’s internal version if not set.

  • logging_level: str The default logging level for the application. Defaults to "DEBUG".

  • logging_config: LoggingConfig | None Provides a logging configuration instance. Defaults to StandardLoggingConfig.

OpenAPI

  • enable_openapi: bool Enables or disables the OpenAPI schema generation for the application.

  • openapi_config: OpenAPIConfig | None Custom configuration for the OpenAPI documentation. See OpenAPI.

  • include_in_schema: bool Controls whether routes are included in the OpenAPI schema by default.

Body Handling

  • infer_body: bool If set to True, Lilya will attempt to infer body types automatically at runtime.

Warning

Lilya has infer_body set to False by default.

Example

from msgspec import Struct
from pydantic import BaseModel


class User(BaseModel):
    name: str
    age: int


class Item(Struct):
    sku: str


async def process_body(user: User, item: Item):
    return {**user.model_dump(), "sku": item.sku}

Where the payload is:

{
    "user": {"name": "lilya", "age": 20},
    "item": {"sku": "test"}
}

Assuming you have the encoders for Pydantic and Struct installed in your application (or any other) you desire.

Lilya uses the internal Encoders to parse and transform them properly.

Note

infer_body is set to False by default but you can override it in the settings.

Another example can be:

from pydantic import BaseModel


class User(BaseModel):
    name: str
    age: int


async def process_body(user: User):
    return user

Where here the post can be directly sent like:

{
    "name": "lilya", "age": 20
}

Routing

  • default_route_pattern: str Default pattern used to locate routes when using the Include helper. Defaults to "route_patterns".

  • enforce_return_annotation: bool Enforces return type annotations on handlers. Raises ImproperlyConfigured if missing.

Middleware

  • middleware: Sequence[DefineMiddleware] List of global middleware applied to the application. All must be wrapped with DefineMiddleware.

Permissions

  • permissions: Sequence[DefinePermission] List of global permissions applied to the application. All must be wrapped with DefinePermission.

Security

  • x_frame_options: str | None Controls the X-Frame-Options response header ("DENY", "SAMEORIGIN", etc.). Used with XFrameOptionsMiddleware.

Events & Lifespan

  • on_startup: Sequence[Callable[[], Any]] List of callables executed on application startup.

  • on_shutdown: Sequence[Callable[[], Any]] List of callables executed on application shutdown.

  • lifespan: ApplicationType | None A lifespan context manager alternative to on_startup/on_shutdown.

  • before_request: Sequence[Callable[..., Any]] | None A list of callables triggered before request processing.

  • after_request: Sequence[Callable[..., Any]] | None A list of callables triggered after request processing.

Exceptions

  • exception_handlers: ExceptionHandler | dict[Any, Any] Global handlers for application exceptions. Can be a dictionary or a single handler.

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.global_settings 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, via request.app.debug directly.