Configue
========

![CI](https://github.com/illuin-tech/configue/workflows/CI/badge.svg)
[![codecov](https://codecov.io/gh/illuin-tech/configue/branch/master/graph/badge.svg)](https://codecov.io/gh/illuin-tech/configue)

A YAML parser with advanced functionalities to ease your application configuration.

# Who is this library for ?
This library is meant to be used in medium to large-scale applications, that have a lot of parameters to configure. 

Modular applications especially can greatly benefit from using `configue` to easily inject new modules.

# Installation

Run `pip install configue` to install from PyPI.

Run `pip install .` to install from sources.

This project follows the (Semantic Versioning Specification)[https://semver.org/].
All breaking changes are described in the [Changelog](CHANGELOG.md). 

# Usage

### Basic usage
This library uses [PyYAML](https://github.com/yaml/pyyaml) to parse the YAML files and return the file content.

```python
import configue


config = configue.load("/path/to/yaml/file.yml")
```


### Loading a sub path
If you are not interested in loading the whole file, you can only load a subpath:
```yaml
# config.yml
some_key:
  some_list:
    - first_item
    - second_item:
        item_key: item_value

not_loaded_key: not_loaded_value
```

```python
import configue

config = configue.load("config.yml", "some_key.some_list.1.item_key")
assert config == "item_value"
```

### Instantiating classes

Use `()` in your YAML files to instantiate classes:
```yaml
# config.yml
(): "my_project.MyAwesomeClass"
my_argument: "my_value"
my_other_argument:
  (): "my_project.my_module.MyOtherClass"
```

```python
import configue
from my_project import MyAwesomeClass
from my_project.my_module import MyOtherClass


my_instance = configue.load("config.yml")
assert isinstance(my_instance, MyAwesomeClass)
assert my_instance.my_argument == "my_value"
assert isinstance(my_instance.my_other_argument, MyOtherClass)
```


### Loading external variables

```yaml
# config.yml
my_argument: !ext my_project.my_module.my_variable
my_argument: !ext my_project.my_module.my_instance.my_attribute
```

When using the `!ext` tag, the value will be imported from the corresponding python module.


### Loading internal variables

```yaml
# config.yml
my_object:
    my_instance:
        (): my_project.MyClass
my_instance_shortcut: !cfg my_object.my_instance
my_attribute_shortcut: !cfg my_object.my_instance.my_attribute
```

When using the `!cfg` tag, the value will be loaded from the same configuration file (useful for a DRY configuration).

### Environment variables

If you want to load an environment variable in your YAML config file, you can use this syntax:
```yaml
# config.yml
my_key: ${VAR_NAME}
```
This will resolve as `"my_value"` if the environment variable `VAR_NAME` is set to this value.

If you need a default value in case the environment variable is not set:
```yaml
# config.yml
my_key: ${VAR_NAME-default}
```

You can insert this syntax in the middle of a string:
```yaml
# config.yml
my_key: prefix${VAR_NAME-default}suffix
```
This will resolve as `"prefixmy_value_suffix"` if the value is set, `"prefixdefaultsuffix"` if it is not.

If your environment variable resolves to a yaml value, it will be cast (unless you are using quotes):
```yaml
# config.yml
my_key: ${VAR_NAME}
my_quoted_key: "${VAR_NAME}"
```
This will resolve as `True` if the value is set to `true`, `yes` or `y`, `None` if the value is set to `~` or `null`.


### Relative paths

If you want to expand a relative path in your YAML config file:

````yaml
# config.yml
my_path: !path my_folder/my_file.txt  
````
Assuming your file structure looks like this:
```
root/
├── config.yml
└── my_folder
    └── my_file.txt
```

The path is resolved starting from the folder containing the parent yml file, this example will resolve to
`/root/my_folder/my_file.txt`

Do not start the path with `/` as it will be treated as an absolute path instead.

You can use environment variables in your file path.

### Importing other files

You can import another file directly in your YAML config file:

````yaml
# config.yml
my_import: !import my_folder/my_other_config.yml
````

```yaml
# my_other_config.yml
some_key:
    - var_1
    - var_2
```

By default, the path is resolved starting from the folder containing the parent yml file, this example will resolve to
`"my_import": {"some_key": [var_1, var_2]}`

If you want to import only a section of the file, use the path in the tag suffix `!import:some_key.0`

Do not start the import path with `/` as it will be treated as an absolute path instead.

You can use environment variables in your import path.

### Logging configuration

You can load the logging configuration for your application by using the `logging_config_path` parameter:
```yaml
# config.yml
logging_config:
  version: 1
  handlers:
    console:
      class : logging.StreamHandler
      stream  : ext://sys.stdout
    custom_handler:
      \(): my_app.CustomHandler
      some_param: some_value
      level: ERROR
  root:
    level: INFO
    handlers:
      - console

app_config:
  some_key: some_value

not_loaded_key: not_loaded_value
```

```python
import logging

import configue

app_config = configue.load("config.yml", "app_config", logging_config_path="logging_config")
assert app_config == {"some_key": "some_value"}

logger = logging.getLogger(__name__)
logger.info("Hello world!")  # Uses the console handler
```

The logging configuration should follow the format of `logging.config.dictConfig`
(check [the documentation](https://docs.python.org/3/library/logging.config.html#logging-config-dictschema) for more
details).
Make sure to escape the constructors with `\()` instead of `()` for handlers, formatters and filters.


# Testing

Install the development dependencies with `pip install -r dev.requirements.txt`.

Run `python -m unitttest discover` to run the tests.

Run `pylint configue` to check the files linting.
