# ConfigSage
`configsage` is a `pyyaml` wrapper converting the configuration parameters taken by a *yaml* file in a *python* object.  
The object reflects the *yaml* hierarchy, allowing the access to the items by a dotted notation.   

!!! example  
    The following example shows a simple *yaml* configuration file with all the use-cases and how they can be accessed by *python* using the `configsage` as configuration parser.  

    ```yaml  
    paths:  # List of directories to scan
      - path: "/path/to/the/first/structure"  # Path to the first folder to scan
        folder_structure:
        - artist  # First folder is artist
        - album   # Second folder is album
        - disc    # Third folder is disc (optional, e.g., for multi-disc albums)
        file_name: ^(?P<nr>\d\d)(?P<title>.*)(?P<ext>\.mp3)  # regex with named groups to get nr = track number, title = track title and extension
        album_name: ^(?P<album>.*) \((?P<year>\d{4})\) # regex with named groups to get album name and year from the "second folder" name
        album_cover: "http://album_covers.org/mycover.jpg" # URL or local path to the cover image
        

      - path: "/path/to/the/second/structure"  # Path to the second folder to scan
        folder_structure:
        - artist  # First folder is artist
        - album   # Second folder is album
        - disc    # Third folder is disc (optional, e.g., for multi-disc albums)
        file_name: ^(?P<nr>\d\d) - (?P<title>.*)(?P<ext>\.mp3)  # regex with named groups to get nr = track number, title = track title and extension
        album_name: ^(?P<album>.*) - (?P<year>\d{4})
        album_cover: "http://album_covers.org/mycover2.jpg" # URL or local path to the cover image

      - path: "/path/to/another/structure"  # Path to another folder to scan (this won't have any parsing in file name and album name so taken as they are)
        folder_structure:
        - genre   # First folder is genre
        - artist  # Second folder is artist
        - album   # Third folder is album
    
    ```

<br/>

## Schema  
When a schema is not provided the library just returns the objectified configuration. 
When a schema is provided, the following steps are done:

- A value marked in the schema as default (by `_default: True`) is added in the configuration if missing. This is done before any check.
- If normalization is enabled, all the values by the flag are normalized according to the functions listed. This is done on scalar values and list of scalars.

The following checks are performed:

- if the config file has unknown keys 
- if the config file has some missing mandatory key
- if the value for the fileds marked as `_enum` are in the enum list
- if the scalar types are the expected in `_type`

The schema supports the following metadata keys, all starting by underscore (`_`)

- `_type`: defines the strict item type (if scalar is str, int, etc. or list[str] - or dict). This is mandatory on list as they need to be list of something. If omitted the check on item type is not performed. 
- `_default`: default value to consider even if the field is not in the config file. This is applied only to scalar values
- `_enum`: allowed possible values for that field. This is applied to scalar values and list of scalar values
- `_normalizers`: functions to call in sequence to normalize. This is applied to scalar values and list of scalar values
- `_validators`: functions to call in sequence to validate. This applies to scalar values and list of scalar values.
- `_required`: if the field is missing or False then the field can be omitted, - otherwise the validation fails

The check workflow is the following:  

> `_default → _normalizers → _enum → _validators → return`

!!! example  
    A schema file example for the previous *yaml* example is  

    ```py
    """
    App-defined schema (with Python types & callables) to validate the configuration
    """

    # schema.py
    from configsage.normalizers import trim_lower
    from configsage.validators import path_exists


    SCHEMA = {
        # list of directories to scan
        "paths": {
            "_type": list[dict],
            "path": {
                "_type": str,
                "_required": True,
                "_validators": [path_exists]
            },
            "folder_structure": {
                "_type": list[str],
                "_enum": {"genre","artist","album","disc"},
                "_normalizers": trim_lower,
            },
            "file_name": {"_type": str},
            "album_name": {"_type": str},
            "album_cover": {"_type": str}
        }
    }
    ```  

    So:  

    - the schema must import the validators and the normalizers written by the user
    - `path` must be a string, must be always present and must exist as path on the file system, as checked by the `path_exists` function.
    - `folder_structure` is a list of strings containing a limited set of values ("genre","artist","album","disc") and the content is trimmed and set to low-case before any check
    - the other fields are all strings

<br/>

## Usage
To use the package it can be installed via *pip*  

```shell  
pip install configsage
```

or via *poetry*  

```shell   
poetry add configsage
```

then it can be used by  

- creating a configuration schema like in the example (schema must import the validators and the normalizers if applicable as they're callables)
- importing your configuration schema into your application
- importing configsage in your application
- creating the Config object with your parameters
- accessing the configuration via dotted-notation


```py  
import schema
from configsage import Config

cfg_file = "myconfig.yaml"

cfg = Config(
        config_src=cfg_file,
        schema=schema,
        validate=True,
        normalize=True, 
    )

print(cfg.paths[0].path) # returns "/path/to/the/first/structure"
print(cfg.paths[1].folder_structure[1]) # returns "album"
```