Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
937d126
BREAKING: Switched Source Model
kzoltner Sep 24, 2026
3f82fe3
ruff format
kzoltner Sep 24, 2026
cfb9cf4
Merge pull request #2 from SmartFactory-KL/feature-add-sources
kzoltner Sep 24, 2026
f932bf2
add support for toml
kzoltner Sep 24, 2026
60f50b3
extend tests
kzoltner Sep 24, 2026
350dacf
Merge pull request #3 from SmartFactory-KL/feature-add-toml
kzoltner Sep 24, 2026
8ae75ab
Default values are masked now as well
kzoltner Sep 24, 2026
e3ce871
fix bool behaviour to not silently go to False on typos.
kzoltner Sep 24, 2026
e57a8a5
Merge pull request #4 from SmartFactory-KL/feature-cover-secrets
kzoltner Sep 24, 2026
46ea713
ruff
kzoltner Sep 24, 2026
8c10417
Merge pull request #5 from SmartFactory-KL/fix-bool-parsing
kzoltner Sep 24, 2026
3f678a6
Add filtering to type_hints
kzoltner Sep 24, 2026
5bfd2f9
Fix file reading order for YAML/TOML
kzoltner Sep 24, 2026
5b8e642
Remove silently defaulting on filesystem errors
kzoltner Sep 24, 2026
4f063ab
removed the strip() and fixed a few other smalls
kzoltner Sep 24, 2026
467e867
formatting fixes
kzoltner Sep 24, 2026
55f2ddc
fix examples
kzoltner Sep 24, 2026
1b2e5de
improve handling of \n and ``` for documents
kzoltner Sep 25, 2026
cbdb932
ruff format
kzoltner Sep 25, 2026
cba3dfb
add tests for invalid inputs
kzoltner Sep 25, 2026
8bd775b
add missing exports
kzoltner Sep 25, 2026
bc3dc74
improved handling of "non-perfect" dataclasses
kzoltner Sep 25, 2026
08812b7
stringify before len()
kzoltner Sep 25, 2026
aea538e
example update
kzoltner Sep 25, 2026
8291247
clarify trim_string docs
kzoltner Sep 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 113 additions & 90 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

# ApPySetty ⚙️

**A simple, type-safe Python library for managing application configuration from environment variables and YAML.**
**A simple, type-safe Python library for managing application configuration from Environment variables and YAML.**

ApPySetty uses Python dataclasses as the single definition of your application configuration. It can load values from configuration files and environment variables and generate documentation from the same definition.

Expand All @@ -15,12 +15,12 @@ Install:
uv add appysetty
```

Define your configuration:
Define your configuration and read from Environment:

```python
from dataclasses import dataclass

from appysetty import AppConfigSource, read_configuration
from appysetty import EnvSource, read_configuration


@dataclass
Expand All @@ -32,22 +32,11 @@ class Config:

config = read_configuration(
Config,
AppConfigSource.ENV,
EnvSource(),
)
```

> [!note]
> Please note that `.strip()` is applied to all string values which removes leading and trailing whitespaces

Override values with environment variables:

```bash
export HOST="0.0.0.0"
export PORT="9000"
export DEBUG="true"
```

Or load them from YAML:
Or load values from YAML:

```yaml
host: localhost
Expand All @@ -58,51 +47,126 @@ debug: false
```python
config = read_configuration(
Config,
AppConfigSource.YAML,
YamlSource(path="yaml_file.yaml"),
)
```

And also document your configuration with an example yaml and a markdown document:
Or both:

```python
config = read_configuration(
Config,
[YamlSource(path="yaml_file.yaml"), EnvSource()],
)
```

> [!note]
> Sources are applied in order. Later sources override values from earlier sources.

And also document your configuration with an example .yaml and a markdown document:

```python
write_configuration_documentation(Config, output_dir=Path("./docs"))
```

## Configuration Sources

ApPySetty currently supports:
ApPySetty uses `AppConfigSource` as the interface to define loaders. These sources are loaded and applied in the order they are provided.

- `ENV`: environment variables using UPPER_SNAKE_CASE only
- `YAML`: YAML configuration files
```python
cfg = read_configuration(
Config, [YamlSource(...), TomlSource(...), EnvSource(...), DictSource(...)]
)
```

In the example above, YAML values are applied first, then environment variables, and finally dictionary values. Later sources override values from earlier sources.

Multiple sources can be combined. Later sources overwrite values from earlier sources:
The available sources are:

#### `EnvSource()` - Reading from Environment

```python
config = read_configuration(
Config,
[
AppConfigSource.YAML,
AppConfigSource.ENV,
],
)
cfg = read_configuration(Config, EnvSource(prefix="MY_PREFIX"))
```

For every key within the config, the key is converted to UPPER_SNAKE_CASE, the optional prefix is applied and the resulting key is used to read a value from the environment.

> [!note]
> The prefix itself will not be converted to UPPER_SNAKE_CASE

#### `DictSource()` - Reading from a Dict

```python
cfg = read_configuration(Config, DictSource(input={"key": "val"}))
```

By default, YAML files are searched for in:
Values are read from the provided dictionary using the configuration field names as keys. Unknown dictionary keys are rejected.

#### `YamlSource()` - Reading from a .yaml file

```python
cfg = read_configuration(Config, YamlSource(path="", required=True))
```

If path is specified, that file is used. Otherwise, the first existing file from the following list is used:

```text
config.yaml
config.yml
config/config.yaml
config.yaml
config/config.yml
config/config.yaml
```

An explicit YAML path can also be provided.
If required is False, a missing file will simply be ignored. If required is True an AppConfigError is raised. By default required is set to True.

## Usage Details
> [!note]
> Only flat mappings are allowed and the YAML key must match the config key exactly

### Define Config
#### `TomlSource()` - Reading from a .toml file

First you need to define a dataclass which contains all the configuration options you want to support.
```python
cfg = read_configuration(Config, TomlSource(path="", required=True))
```

If path is specified, that file is used. Otherwise, the first existing file from the following list is used:

```text
config.toml
config/config.toml
```

If required is False, a missing file will simply be ignored. If required is True an AppConfigError is raised. By default required is set to True.

> [!note]
> Only flat mappings are allowed and the TOML key must match the config key exactly

#### Define your own source

All sources are based on the `AppConfigSource`. To extend the list of sources, you could supply your own implementation:

```python
@dataclass(frozen=True)
class MyOwnSource(AppConfigSource):
"""Example for your source, based on the DictSource"""

input: dict[str, str]

def load(self, config_type_hints):
values: dict[str, object] = {}

for name, value in self.input.items():
...

return values
```

> [!caution]
> Using your own source might allow for more types then anticipated by the tool. So be careful.


## Define Config

The simplest form of a config class looks like this:

```python
@dataclass
Expand All @@ -114,7 +178,7 @@ class Config:
```

> [!note]
> As of now, only `str`, `int`, `float` and `bool` are supported. Experience shows that other types are usually better handled on the user side
> As of now, only `str`, `int`, `float` and `bool` are supported

You can also extend your dataclass with additional information for better documentation and for masking secrets:

Expand All @@ -130,63 +194,18 @@ class ConfigWithMetadata:
str,
AppConfigEntry(description="The database password", is_secret=True),
] = "secret"
```

Both variants can be mixed. If no description is provided, the name of the field will be the description.

### Read Config

To read the config, use:

```python
cfg = read_configuration(ConfigWithMetadata, AppConfigSource.ENV)
```

After that `cfg` should have all configurations with auto-complete ready for you.

You can also pass an instance and use multiple sources, where each source will overwrite the previous one:

```python
cfg = read_configuration(
ConfigWithMetadata(), [AppConfigSource.ENV, AppConfigSource.YAML]
)
```

The available sources are:

| Key | Source | Description |
| ---------------------- | ----------- | ---------------------------------------------------------------------------------------- |
| `AppConfigSource.ENV` | Environment | This will read config from environment, using UPPER_SNAKE_CASE variant of the field name |
| `AppConfigSource.YAML` | YAML file | This will read the config from a yaml file, only matching field name exactly |

#### Options

Options can be used to customize the config:

```python
cfg = read_configuration(
ConfigWithMetadata,
[AppConfigSource.ENV, AppConfigSource.YAML],
AppConfigOptions(
env_prefix="MY_APP_PREFIX",
yaml_path="config.dev.yaml",
overwrite={"port": "8000"},
),
)
debug: bool = False
```

| Option | Description |
| ------------ | ----------------------------------------------------------------------------------------------- |
| `env_prefix` | A Prefix that will be prepended to all field names using UPPER_SNAKE_CASE to read environment . |
| `yaml_path` | Setting a specific yaml file to use. It unset, the tool will look for `(config)/config.y(a)ml`. |
| `overwrite` | This accepts a mapping. values set with overwrite will always overwrite anything else |
Both variants can be mixed. If no description is provided, the name of the field will be the description.

### Write Documentation
## Write Documentation

The second feature of this tool is automated creation of a few documentation items for configuration options:
One feature of this tool is automating the documentation items for configuration options:

- `config.example.yaml` containing an example yaml file with default values and descriptive comments (if descriptions were defined)
- `DefaultConfiguration.md` containing a table of all options with ENV variant, a docker environment block for docker compose and a docker run example command with all -e set.
- `config.example.yaml` containing an example YAML file with default values and descriptive comments (if descriptions were defined)
- `DefaultConfiguration.md` containing a table of all options with ENV variant, a docker compose `environment` block for docker compose and a docker run example command with all -e set.

To create the documentation, use:

Expand All @@ -196,7 +215,7 @@ write_configuration_documentation(
ConfigWithMetadata, env_prefix="MY_APP_PREFIX", output_dir=Path()
)

# Only create yaml example
# Only create YAML example
write_config_yaml_example(ConfigWithMetadata, output_dir=Path())

# Only create markdown document
Expand All @@ -206,6 +225,9 @@ write_config_markdown(ConfigWithMetadata, env_prefix="MY_APP_PREFIX", output_dir
> [!note]
> `env_prefix` defaults to "" if not set and `output_dir` defaults to `./docs/config` if not set.

> [!caution]
> Make sure to always match the `env_prefix` to the actual prefix used for the EnvSource if applied

## Development

Clone the repository and install the development dependencies:
Expand Down Expand Up @@ -235,7 +257,8 @@ uv run python -m examples.write_documentation
uv run python -m examples.read_documentation
```

Ruffing:
Run Ruff:

```bash
uv run ruff check
uv run ruff format .
Expand Down
4 changes: 2 additions & 2 deletions examples/define_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class ExampleConfig:
description="Host to run the application on",
is_secret=False,
),
] = "localhost"
] = "localhost\nbadboy"

# You can also only annotate what actually needs a description - unlike this port
port: int = 8080
Expand All @@ -35,4 +35,4 @@ class ExampleConfig:
description="API key used to access external services",
is_secret=True,
),
] = ""
] = "not here"
22 changes: 11 additions & 11 deletions examples/example_output/DefaultConfiguration.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,25 @@

| ENV | Variable | Type | Default | Is Secret | Description |
|---|---|---|---|---|---|
| EXAMPLE_APP_HOST | host | `str` | `localhost` | False | Host to run the application on |
| EXAMPLE_APP_PORT | port | `int` | `8080` | False | port |
| EXAMPLE_APP_DEBUG | debug | `bool` | `False` | False | debug |
| EXAMPLE_APP_TIMEOUT | timeout | `float` | `5.0` | False | Request timeout in seconds |
| EXAMPLE_APP_WORKERS | workers | `int` | `4` | False | workers |
| EXAMPLE_APP_API_KEY | api_key | `str` | `` | True | API key used to access external services |
| EXAMPLE_APP_HOST | host | str | localhost\nbadboy | False | Host to run the application on |
| EXAMPLE_APP_PORT | port | int | 8080 | False | port |
| EXAMPLE_APP_DEBUG | debug | bool | False | False | debug |
| EXAMPLE_APP_TIMEOUT | timeout | float | 5.0 | False | Request timeout in seconds |
| EXAMPLE_APP_WORKERS | workers | int | 4 | False | workers |
| EXAMPLE_APP_API_KEY | api_key | str | Masked[len:8] | True | API key used to access external services |

## Docker Compose

Example environment block using the default values:

```yaml
environment:
EXAMPLE_APP_HOST: localhost
EXAMPLE_APP_HOST: localhost\nbadboy
EXAMPLE_APP_PORT: 8080
EXAMPLE_APP_DEBUG: False
EXAMPLE_APP_DEBUG: false
EXAMPLE_APP_TIMEOUT: 5.0
EXAMPLE_APP_WORKERS: 4
EXAMPLE_APP_API_KEY:
EXAMPLE_APP_API_KEY: Masked[len:8]
```

## Docker Run
Expand All @@ -31,11 +31,11 @@ Example `docker run` command using the default values:

```bash
docker run \
-e EXAMPLE_APP_HOST=localhost \
-e EXAMPLE_APP_HOST='localhost\nbadboy' \
-e EXAMPLE_APP_PORT=8080 \
-e EXAMPLE_APP_DEBUG=False \
-e EXAMPLE_APP_TIMEOUT=5.0 \
-e EXAMPLE_APP_WORKERS=4 \
-e EXAMPLE_APP_API_KEY=''
-e EXAMPLE_APP_API_KEY='Masked[len:8]' \
your-image:latest
```
Loading
Loading