Skip to main content
[![coverage report](https://gitdma.risc-software.at/risc_ds/lasagna/badges/master/coverage.svg)](https://gitdma.risc-software.at/risc_ds/lasagna/-/commits/master)
[![pipeline status](https://gitdma.risc-software.at/risc_ds/lasagna/badges/master/pipeline.svg)](https://gitdma.risc-software.at/risc_ds/lasagna/-/commits/master)

# Lasagna

A package that makes it easy to load configuration variables from
multiple sources and build a single config data class.

## The Idea
The idea is to 'stack' the sources like lasagna and read them from the
bottom up. This means the layer at the bottom can set defaults and any
layers on top of it overwrite these defaults. Which allows to build
configurations from multiple sources with one clear end result.

The configuration must be wrapped up in a single data class.
This enables the use of type hints as well as taking advantage of any other standard tooling
which operates on data classes. The library also supports nested data classes. This results in
multiple *advantages*:

- When refactoring, for example adding a required field to the dataclass,
without adding it to the default config, results in a error on startup.
- Type annotations can support the developer. No more guessing what is read.
- Automatically parsing simple types from configuration files and the environment
- No boilerplate for accessing dicts / ...
- Easily combine multiple sources for the environment **(data class instances, .env files, environment)**

## Setup

No additional packages are currently required to use `lasagna`. Simply install it.
```shell script
pip install risc-lasagna
```

## Development

For development simply clone the repository. To release a version change the version specifier
in the `setup.cfg` and commit the changes with a tag. The tag name should be `<VERSION_IN_SETUP_CFG>` (e.g. 2.0.2).

## Tests

See task in `pyproject.toml`.

## Examples

The following section contains a few examples whiche illustrate how to use the package.

### Simple Example
To build your config you need to first specify a dataclass which
encompasses the necessary values. The next step is to specify where the
values come from. A simple example is to use the `EnvironmentLayer`. Which
reads all values from the environment and loads them in the dataclass.

```python
from dataclasses import dataclass
import os # for testing purposes

import risc_lasagna as lasagna
import risc_lasagna.layer as layer


@dataclass
class ApplicationConfig:
app_host: str
app_port: int


def load_application_config() -> ApplicationConfig:
# add environment for testing, this should be set by your deployment tool
os.environ['APP_HOST'] = 'localhost'
os.environ['APP_PORT'] = '12345'
return lasagna.build(ApplicationConfig, layer.EnvironmentLayer())
```
Executing this example results in a simple parsed application config
```python
>>> load_application_config()
ApplicationConfig(app_host='localhost', app_port=12345)
```

### Simple Example with default layer
Sometimes it is easier to provide 'defaults' for every dataclass. This can be accomplished by providing
a default layer using your dataclass. The same example as before can be rewritten to use a default
layers using the `DataClassDefaultLayer` and the `AggregationLayer`. The `AggregationLayer`
Simply combines the layers from top to bottom. The bottom most layer is overwritten
by every the top layers.

```python
from dataclasses import dataclass
import os # for testing purposes

import risc_lasagna as lasagna
import risc_lasagna.layer as layer


@dataclass
class ApplicationConfig:
app_host: str
app_port: int


def load_application_config() -> ApplicationConfig:
os.environ['APP_HOST'] = 'remote_host'
return lasagna.build(ApplicationConfig,
[
layer.EnvironmentLayer(),
layer.DataClassDefaultLayer(
ApplicationConfig('localhost', 12345)
),
],
)
```
Running the example results in the `EnvironmentLayer` overwriting the `APP_HOST` variable of the
`DataClassDefaultLayer`.
```python
>>> load_application_config()
ApplicationConfig(app_host='remote_host', app_port=12345)
```

### Example with nested dataclass
For larger configurations it might be easier or convenient
to assemble it from multiple dataclasses. This is possible by
prefixing the existing variables in the environment with the name of
the property. Here is an example:
```python
from dataclasses import dataclass
import os # for testing purposes
from typing import Optional

import risc_lasagna as lasagna
import risc_lasagna.layer as layer


@dataclass
class DatabaseConfig:
host: str
port: int
user: Optional[str] = None
password: Optional[str] = None


@dataclass
class ApplicationConfig:
app_host: str
app_port: int
database_config: DatabaseConfig # the name of this variable is used as a prefix


def load_application_config() -> ApplicationConfig:
# set the environment variable with a prefix
os.environ['DATABASE_CONFIG_HOST'] = 'db-host'
return lasagna.build(
ApplicationConfig,
[
layer.EnvironmentLayer(),
layer.DataClassDefaultLayer(
ApplicationConfig('localhost', 12345, DatabaseConfig('localhost', 5432))
),
],
)
```

The example results in the `EnvironmentLayer` overwriting the defaults:

```python
>>> load_application_config()
ApplicationConfig(app_host='localhost', app_port=12345, database_config=DatabaseConfig(host='db-host', port=5432, user=None, password=None))
```

### Example with YAML and lists
For more complex configuration YAML is a better option. Yaml also supports lists and objects inside lists.
>HINT: If you use lists with nested dataclasses be aware that only a single type per list is supported.

```python
import tempfile # for testing purposes
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional, List

import risc_lasagna as lasagna
import risc_lasagna.layer as layer


@dataclass
class DatabaseConfig:
host: str
port: int
user: Optional[str] = None
password: Optional[str] = None


@dataclass
class HealthCheck:
rate_in_ms: int
url: str


@dataclass
class ApplicationConfig:
host: str
port: int
elements: List[str]
health_check: Optional[HealthCheck] = field(default=None)
protocol: str = field(default='https')
database_configs: Optional[List[DatabaseConfig]] = field(default=None)


__example_yaml = """
host: remote_host
health_check:
rate_in_ms: 30
url: localhost:6543
elements:
- abc
- def
- ghi
database_configs:
- host: remote_db
port: 1234
- host: remote_db
port: 1234
user: postgres
password: super_safe_password
"""


def load_application_config() -> ApplicationConfig:
# set the environment variable with a prefix
with tempfile.NamedTemporaryFile() as temp:
temp.write(bytes(__example_yaml, encoding='utf-8'))
temp.flush()
return lasagna.build(
ApplicationConfig,
[
layer.YamlLayer(Path(temp.name)),
layer.DataClassDefaultLayer(
ApplicationConfig(
'localhost', 12345, ['a', 'b', 'c'],
database_configs=[DatabaseConfig('localhost', 5432)]
)
),
],
)
```

Executing the example results in this dataclass:
```
>>> load_application_config()
ApplicationConfig(host='remote_host', port=12345, elements=['abc', 'def', 'ghi'], health_check=HealthCheck(rate_in_ms=30, url='localhost:6543'), protocol='https', database_configs=[DatabaseConfig(host='remote_db', port=1234, user=None, password=None), DatabaseConfig(host='remote_db', port=1234, user='postgres', password='super_safe_password')])
```

### FAQ

#### What happens if not enough variables are present to instantiate the data class?
In this case python raises an Error when the constructor is called ('... missing required attribute ...'). There are
two options to avoid the error:
1. Add the necessary variable to the environment
2. Make the attribute optional

#### I have a global prefix ('RISCSW') which is prepended before any variable, do i need to add this to my variable names?
No. The `build` methods defines a `variable_prefix` parameter which can be used to deal with such cases.
Other layers may also include a `variable_prefix` which is then applied to all of the variables in the layer.

#### I want to read .env files. Is this possible?
Yes. You can use the `DotEnvLayer` for this. This layer also supports comment in the .env file.
The file path has to be provided - Either make it fixed or write it to another environment variable, to set it
dynamically.

#### I want to read .yml files. Is this possible?
Yes. Use the YamlLayer. This has a few limitations (directly nested lists are not supported).
But works nicely for most cases.

#### I want to read complex types (such as np.array, Unions)
Currently only simple types are supported. If you want to contribute contact the maintainer. Optional is the only
supported type.

Metadata

Release files for risc-lasagna 2.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for risc-lasagna 2.0.0
File Size Uploaded Provenance
risc_lasagna-2.0.0.tar.gz 16.2 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for risc-lasagna 2.0.0
File Interpreter ABI Platform
risc_lasagna-2.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 35.2 kB

Release files / risc_lasagna-2.0.0.tar.gz

Download URL risc_lasagna-2.0.0.tar.gz
Size 16.2 kB
Tags Source
SHA-256 checksum
How to use checksums
3c376e76cfe00f10a0c7fb8a8d96ba6175001a808c7dab577b1cfda4153670a2
BLAKE2b-256 checksum
How to use checksums
8f68f95b0ee862fd89400ff7efdd5e6ea25ebf06688847796f9238a23e5dc05e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.8.20

Release files / risc_lasagna-2.0.0-py3-none-any.whl

Download URL risc_lasagna-2.0.0-py3-none-any.whl
Size 19.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7d72d6ca9d003cbf14f8949bf27b7edd0f6a540f7b33839a8441598462d99779
BLAKE2b-256 checksum
How to use checksums
375e5ed5552539acc112b4f1c131a4c32b28c0426a7ac09d423e156d73578ff8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.8.20

Security for risc-lasagna 2.0.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page