# -*- coding: utf-8 -*-
from setuptools import setup

packages = \
['risc_lasagna', 'risc_lasagna.examples', 'risc_lasagna.layer']

package_data = \
{'': ['*']}

install_requires = \
['PyYAML>=6.0,<7.0']

setup_kwargs = {
    'name': 'risc-lasagna',
    'version': '2.0.0',
    'description': 'A package that makes it easy to load configuration variables from multiple sources and build a single config which depends on them.',
    'long_description': '[![coverage report](https://gitdma.risc-software.at/risc_ds/lasagna/badges/master/coverage.svg)](https://gitdma.risc-software.at/risc_ds/lasagna/-/commits/master)\n[![pipeline status](https://gitdma.risc-software.at/risc_ds/lasagna/badges/master/pipeline.svg)](https://gitdma.risc-software.at/risc_ds/lasagna/-/commits/master) \n\n# Lasagna\n\nA package that makes it easy to load configuration variables from \nmultiple sources and build a single config data class.\n\n## The Idea\nThe idea is to \'stack\' the sources like lasagna and read them from the \nbottom up. This means the layer at the bottom can set defaults and any \nlayers on top of it overwrite these defaults. Which allows to build \nconfigurations from multiple sources with one clear end result.\n\nThe configuration must be wrapped up in a single data class. \nThis enables the use of type hints as well as taking advantage of any other standard tooling\nwhich operates on data classes. The library also supports nested data classes. This results in \nmultiple *advantages*:\n\n- When refactoring, for example adding a required field to the dataclass, \nwithout adding it to the default config, results in a error on startup.\n- Type annotations can support the developer. No more guessing what is read.\n- Automatically parsing simple types from configuration files and the environment\n- No boilerplate for accessing dicts / ...\n- Easily combine multiple sources for the environment **(data class instances, .env files, environment)**\n\n## Setup\n\nNo additional packages are currently required to use `lasagna`. Simply install it.\n```shell script\npip install risc-lasagna\n```\n\n## Development\n\nFor development simply clone the repository. To release a version change the version specifier\nin the `setup.cfg` and commit the changes with a tag. The tag name should be `<VERSION_IN_SETUP_CFG>` (e.g. 2.0.2).\n\n## Tests\n\nSee task in `pyproject.toml`.\n\n## Examples\n\nThe following section contains a few examples whiche illustrate how to use the package.\n\n### Simple Example\nTo build your config you need to first specify a dataclass which \nencompasses the necessary values. The next step is to specify where the\nvalues come from. A simple example is to use the `EnvironmentLayer`. Which \nreads all values from the environment and loads them in the dataclass.\n\n```python\nfrom dataclasses import dataclass\nimport os  # for testing purposes\n\nimport risc_lasagna as lasagna\nimport risc_lasagna.layer as layer\n\n\n@dataclass\nclass ApplicationConfig:\n    app_host: str\n    app_port: int\n\n\ndef load_application_config() -> ApplicationConfig:\n    # add environment for testing, this should be set by your deployment tool\n    os.environ[\'APP_HOST\'] = \'localhost\'\n    os.environ[\'APP_PORT\'] = \'12345\'\n    return lasagna.build(ApplicationConfig, layer.EnvironmentLayer())\n```\nExecuting this example results in a simple parsed application config\n```python\n>>> load_application_config()\nApplicationConfig(app_host=\'localhost\', app_port=12345)\n```\n\n### Simple Example with default layer\nSometimes it is easier to provide \'defaults\' for every dataclass. This can be accomplished by providing\na default layer using your dataclass. The same example as before can be rewritten to use a default \nlayers using the `DataClassDefaultLayer` and the `AggregationLayer`. The `AggregationLayer` \nSimply combines the layers from top to bottom. The bottom most layer is overwritten\nby every the top layers. \n\n```python\nfrom dataclasses import dataclass\nimport os  # for testing purposes\n\nimport risc_lasagna as lasagna\nimport risc_lasagna.layer as layer\n\n\n@dataclass\nclass ApplicationConfig:\n    app_host: str\n    app_port: int\n\n\ndef load_application_config() -> ApplicationConfig:\n    os.environ[\'APP_HOST\'] = \'remote_host\'\n    return lasagna.build(ApplicationConfig, \n        [\n            layer.EnvironmentLayer(),\n            layer.DataClassDefaultLayer(\n                ApplicationConfig(\'localhost\', 12345)\n            ),\n        ],\n    )\n```\nRunning the example results in the `EnvironmentLayer` overwriting the `APP_HOST` variable of the\n`DataClassDefaultLayer`.\n```python\n>>> load_application_config()\nApplicationConfig(app_host=\'remote_host\', app_port=12345)\n```\n\n### Example with nested dataclass\nFor larger configurations it might be easier or convenient \nto assemble it from multiple dataclasses. This is possible by \nprefixing the existing variables in the environment with the name of \nthe property. Here is an example:\n```python\nfrom dataclasses import dataclass\nimport os  # for testing purposes\nfrom typing import Optional\n\nimport risc_lasagna as lasagna\nimport risc_lasagna.layer as layer\n\n\n@dataclass\nclass DatabaseConfig:\n    host: str\n    port: int\n    user: Optional[str] = None\n    password: Optional[str] = None\n\n\n@dataclass\nclass ApplicationConfig:\n    app_host: str\n    app_port: int\n    database_config: DatabaseConfig  # the name of this variable is used as a prefix\n\n\ndef load_application_config() -> ApplicationConfig:\n    # set the environment variable with a prefix\n    os.environ[\'DATABASE_CONFIG_HOST\'] = \'db-host\'\n    return lasagna.build(\n        ApplicationConfig,\n        [\n            layer.EnvironmentLayer(),\n            layer.DataClassDefaultLayer(\n                ApplicationConfig(\'localhost\', 12345, DatabaseConfig(\'localhost\', 5432))\n            ),\n        ],\n    )\n```\n\nThe example results in the `EnvironmentLayer` overwriting the defaults:\n\n```python\n>>> load_application_config()\nApplicationConfig(app_host=\'localhost\', app_port=12345, database_config=DatabaseConfig(host=\'db-host\', port=5432, user=None, password=None))\n```\n\n### Example with YAML and lists\nFor more complex configuration YAML is a better option. Yaml also supports lists and objects inside lists.\n>HINT: If you use lists with nested dataclasses be aware that only a single type per list is supported.\n\n```python\nimport tempfile  # for testing purposes\nfrom dataclasses import dataclass, field\nfrom pathlib import Path\nfrom typing import Optional, List\n\nimport risc_lasagna as lasagna\nimport risc_lasagna.layer as layer\n\n\n@dataclass\nclass DatabaseConfig:\n    host: str\n    port: int\n    user: Optional[str] = None\n    password: Optional[str] = None\n\n\n@dataclass\nclass HealthCheck:\n    rate_in_ms: int\n    url: str\n\n\n@dataclass\nclass ApplicationConfig:\n    host: str\n    port: int\n    elements: List[str]\n    health_check: Optional[HealthCheck] = field(default=None)\n    protocol: str = field(default=\'https\')\n    database_configs: Optional[List[DatabaseConfig]] = field(default=None)\n\n\n__example_yaml = """\nhost: remote_host\nhealth_check:\n  rate_in_ms: 30\n  url: localhost:6543\nelements:\n  - abc\n  - def\n  - ghi\ndatabase_configs:\n  - host: remote_db\n    port: 1234\n  - host: remote_db\n    port: 1234\n    user: postgres\n    password: super_safe_password\n"""\n\n\ndef load_application_config() -> ApplicationConfig:\n    # set the environment variable with a prefix\n    with tempfile.NamedTemporaryFile() as temp:\n        temp.write(bytes(__example_yaml, encoding=\'utf-8\'))\n        temp.flush()\n        return lasagna.build(\n            ApplicationConfig,\n            [\n                layer.YamlLayer(Path(temp.name)),\n                layer.DataClassDefaultLayer(\n                    ApplicationConfig(\n                        \'localhost\', 12345, [\'a\', \'b\', \'c\'],\n                        database_configs=[DatabaseConfig(\'localhost\', 5432)]\n                    )\n                ),\n            ],\n        )\n```\n\nExecuting the example results in this dataclass:\n```\n>>> load_application_config() \nApplicationConfig(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\')])\n```\n\n### FAQ\n\n#### What happens if not enough variables are present to instantiate the data class?\nIn this case python raises an Error when the constructor is called (\'... missing required attribute ...\'). There are\ntwo options to avoid the error:\n1. Add the necessary variable to the environment\n2. Make the attribute optional\n\n#### I have a global prefix (\'RISCSW\') which is prepended before any variable, do i need to add this to my variable names?\nNo. The `build` methods defines a `variable_prefix` parameter which can be used to deal with such cases.\nOther layers may also include a `variable_prefix` which is then applied to all of the variables in the layer.\n\n#### I want to read .env files. Is this possible?\nYes. You can use the `DotEnvLayer` for this. This layer also supports comment in the .env file.\nThe file path has to be provided - Either make it fixed or write it to another environment variable, to set it\ndynamically.\n\n#### I want to read .yml files. Is this possible?\nYes. Use the YamlLayer. This has a few limitations (directly nested lists are not supported). \nBut works nicely for most cases. \n\n#### I want to read complex types (such as np.array, Unions)\nCurrently only simple types are supported. If you want to contribute contact the maintainer. Optional is the only \nsupported type.\n',
    'author': 'Dominik Falkner',
    'author_email': 'dominik.falkner@risc-software.at',
    'maintainer': 'None',
    'maintainer_email': 'None',
    'url': 'None',
    'packages': packages,
    'package_data': package_data,
    'install_requires': install_requires,
    'python_requires': '>=3.8,<4.0',
}


setup(**setup_kwargs)
