minaconf 1.1.0
Library for creating a configuration from multiple sources.
License: MIT
Requires Python: >=3.9,<4.0

# minaconf

Library for creating a configuration from multiple sources. The supported
sources are the following ordered by priority from highest to lowest. If a
configuration option is set twice the value from the source with the higher
priority will be used.
1. Initial configuration provided programmatically
2. Cmdline arguments
3. Configuration files (a later one has higher priority than a previous one)
4. Environment variables
5. Default value (if set)

## Installation

Run the following to install:

```python
pip install minaconf
```

## Usage

As example we configure a server with a hostname and a port. First, we'll see
how to use the different sources and later a complete example with multiple
sources will be shown.

Note: The configuration description `cfg_desc` describes the configuration
options of the configuration. It is used to map values from most of the sources
to keys in the configuration dictionary.

### Use Default Values

```python
from minaconf import Configuration

cfg_desc = [
    # configuration option hostname described as dict
    {
        # path of hostname in the final configuration dictionary
        'path': 'server/hostname',
        # default value of hostname
        'default': '0.0.0.0',
    },
    # port
    {'path': 'server/port', 'default': 80}
]

c = Configuration(cfg_desc)
print(c) # Output: {'server': {'hostname': '0.0.0.0', 'port': 80}}
```

### Use Environment Variables

```python
from minaconf import Configuration

cfg_desc = [
    # configuration option hostname described as dict
    {
        # path of hostname in the final configuration dictionary
        'path': 'server/hostname',
        # name of the related env variable
        'env': 'HOSTNAME',
    },
    # port
    {'path': 'server/port', 'env': 'PORT'}
]

c = Configuration(cfg_desc)

print(c)
# $ HOSTNAME=127.0.0.1 PORT=80 python prog.py
# Output: {'server': {'hostname': '127.0.0.1', 'port': '80'}}
#
# $ HOSTNAME=127.0.0.1 python prog.py
# Output: {'server': {'hostname': '127.0.0.1'}}
```

### Use Configuration Files

We provide two config files. `config.yml` is the general configuraiton file and
`config_dev.yml` overrides the port, e.g., for development purposes.

```yml
# config.yml
server:
    hostname: 127.0.0.1
    port: 80
```

```yml
# config_dev.yml
server:
    port: 8080
```

```python
import sys
from minaconf import Configuration

c = Configuration(args=sys.argv[1:])

print(c)
# $ python prog.py config.yml
# Output: {'server': {'hostname': '127.0.0.1', 'port': 80},
#          'configfiles': [PosixPath('config.yml')]}
#
# $ python prog.py config.yml config_dev.yml
# Output: {'server': {'hostname': '127.0.0.1', 'port': 8080},
#          'configfiles': [PosixPath('config.yml'), PosixPath('config_dev.yml')]}
```

Note: The imported configuration files will be stored in `conf['configfiles']`.

### Use Command-line Arguments

```python
from argparse import ArgumentParser
import sys
from minaconf import Configuration

# create your agrument parser
parser = ArgumentParser(description='My Parser')
parser.add_argument('--hostname', type=str)
parser.add_argument('--port', type=int)

cfg_desc = [
    # configuration option hostname described as dict
    {
        # path of hostname in the final configuration dictionary
        'path': 'server/hostname',
        # name of the related cmdline option
        'cmdName': 'hostname',
    },
]

c = Configuration(cfg_desc, parser, sys.argv[1:])
print(c)
# $ python prog.py --hostname 127.0.0.1 --port 80
# Output: {'server': {'hostname': '127.0.0.1'}, 'port': 80, 'configfiles': []}
```

Note: As `port` is not part of the configuration description and, therefore,
has no path it will be added to the base level of the configuration dictionary.

### Use an Initial Dictionary

```python
from minaconf import Configuration

init = {
    'server': {
        'hostname': '127.0.0.1',
        'port': 80,
    },
}

c = Configuration(init_cfg=init)
print(c) # Output: {'server': {'hostname': '127.0.0.1', 'port': 80}}
```

### Required Options

```python
from minaconf import Configuration

cfg_desc = [
    # configuration option hostname described as dict
    {
        # path of hostname in the final configuration dictionary
        'path': 'server/hostname',
        # hostname must not be None
        'required': True,
    },
]

c = Configuration(cfg_desc) # ConfigurationException: server/hostname is not
                            # configured: None
```

### Get Values

```python
from minaconf import Configuration

init = {
    'server': {
        'hostname': '127.0.0.1',
        'port': 80,
    },
}

c = Configuration(init_cfg=init)
print(c['server']['hostname']) # Output: '127.0.0.1'
print(c.get_value('server/port')) # Output: 80
```

### Complete Example

```python
from argparse import ArgumentParser
import sys
from minaconf import Configuration

# create your agrument parser
parser = ArgumentParser(description='My Parser')
parser.add_argument('--hostname', type=str)
parser.add_argument('--port', type=int)

cfg_desc = [
    # configuration option hostname described as dict
    {
        # path of hostname in the final configuration dictionary
        'path': 'server/hostname',
        # default value of hostname
        'default': '0.0.0.0',
        # name of the related cmdline option
        'cmdName': 'hostname',
        # name of the related env variable
        'env': 'HOSTNAME',
        # hostname must not be None
        'required': True,
    },
    {'path': 'server/port', 'default': 80, 'cmdName': 'port', 'env': 'PORT',
     'required': True}
]

init_cfg = {
    'server': {
        'hostname': '127.0.0.1',
    },
}

c = Configuration(cfg_desc, parser, sys.argv[1:], init_cfg)
print(c)
```

Configuration file paths can be added to the program call. The configuration
files must be in yaml format. If you provide an argument parser it will be
extended automatically by
```python
parser.add_argument('configfiles', type=Path, nargs='*', help='paths to configuration files')
```

## Developing minaconf

To develop minaconf run the following commands:

```bash
# install dependencies
$ poetry install

# run tests
$ poetry run pytest tests/

# run flake8
$ poetry run flake8 minaconf/ tests/

# run mypy
$ poetry run mypy minaconf/ tests/

# build
$ poetry build

# publish
$ poetry publish
```

If you use .vscode your settings.json may look similar to the following:

```json
{
    "python.testing.pytestArgs": [
        "tests"
    ],
    "python.testing.unittestEnabled": false,
    "python.testing.pytestEnabled": true,
    "python.linting.flake8Enabled": true,
    "python.linting.enabled": true,
    "python.linting.mypyEnabled": true,
    "files.exclude": {
        "**/.git": true,
        "**/.svn": true,
        "**/.hg": true,
        "**/CVS": true,
        "**/.DS_Store": true,
        "**/Thumbs.db": true,
        "**/.pytest_cache": true,
        "**/__pycache__": true,
        "**/.venv": true,
        ".mypy_cache": true,
        ".tox": true
    }
}
```