├── .github
└── workflows
│ ├── develop_docs.yml
│ ├── python-publish.yml
│ ├── release_docs.yml
│ └── tests.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── docs
├── changelog.md
├── configurator.md
├── css
│ └── code_select.css
├── exceptions.md
├── flask-custom-example.md
├── flask-simple-example.md
├── index.md
├── installation.md
├── requirements.txt
└── usage-examples.md
├── examples
├── custom
│ ├── __init__.py
│ ├── app.py
│ ├── extensions
│ │ └── config.py
│ └── utils.py
└── standard
│ ├── __init__.py
│ ├── app.py
│ └── extensions
│ └── config.py
├── mkdocs.yml
├── overrides
└── main.html
├── pyproject.toml
├── requirements-dev.txt
├── requirements.txt
├── src
└── simple_toml_configurator
│ ├── __init__.py
│ ├── exceptions.py
│ └── toml_configurator.py
└── tests
└── test_toml_configurator.py
/.github/workflows/develop_docs.yml:
--------------------------------------------------------------------------------
1 | name: Build/Publish Develop Docs
2 | on:
3 | push:
4 | branches:
5 | - main
6 | permissions:
7 | contents: write
8 | jobs:
9 | deploy:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v3
13 | with:
14 | fetch-depth: 0
15 | - uses: actions/setup-python@v4
16 | with:
17 | python-version: 3.10.6
18 | - name: Install Dependencies
19 | run: |
20 | pip install -r docs/requirements.txt
21 | pip install pillow cairosvg mike
22 | - name: Setup Docs Deploy
23 | run: |
24 | git config --global user.name "Docs Deploy"
25 | git config --global user.email "docs.deploy@example.co.uk"
26 | - name: Build Docs Website
27 | run: mike deploy --push develop
--------------------------------------------------------------------------------
/.github/workflows/python-publish.yml:
--------------------------------------------------------------------------------
1 | name: Upload Python Package
2 |
3 | on:
4 | release:
5 | types:
6 | - released
7 | workflow_dispatch:
8 |
9 | jobs:
10 | deploy:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v2
15 | - name: Set up Python
16 | uses: actions/setup-python@v1
17 | with:
18 | python-version: '3.x'
19 | - name: Install dependencies
20 | run: |
21 | python -m pip install --upgrade pip
22 | pip install setuptools wheel twine build
23 | - name: Build and publish
24 | env:
25 | TWINE_USERNAME: __token__
26 | TWINE_PASSWORD: ${{ secrets.PYPI_API_KEY }}
27 | run: |
28 | python -m build
29 | twine upload dist/*
--------------------------------------------------------------------------------
/.github/workflows/release_docs.yml:
--------------------------------------------------------------------------------
1 | name: Build/Publish Latest Release Docs
2 | on:
3 | release:
4 | types: [published]
5 |
6 | permissions:
7 | contents: write
8 | jobs:
9 | deploy:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v3
13 | with:
14 | fetch-depth: 0
15 | - uses: actions/setup-python@v4
16 | with:
17 | python-version: 3.10.6
18 | - name: Install Dependencies
19 | run: |
20 | pip install -r docs/requirements.txt
21 | pip install pillow cairosvg mike
22 | - name: Setup Docs Deploy
23 | run: |
24 | git config --global user.name "Docs Deploy"
25 | git config --global user.email "docs.deploy@example.co.uk"
26 | - name: Build Docs Website
27 | run: mike deploy --push --update-aliases ${{ github.event.release.tag_name }} latest
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
3 |
4 | name: Test Package
5 |
6 | on:
7 | push:
8 | branches: [ "main" ]
9 | pull_request:
10 | branches: [ "main" ]
11 |
12 | permissions:
13 | contents: read
14 |
15 | jobs:
16 | build:
17 |
18 | runs-on: ubuntu-latest
19 |
20 | steps:
21 | - uses: actions/checkout@v3
22 | - name: Set up Python 3.10
23 | uses: actions/setup-python@v3
24 | with:
25 | python-version: "3.10"
26 | - name: Install dependencies
27 | run: |
28 | python -m pip install --upgrade pip
29 | pip install flake8 pytest
30 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
31 | pip install .
32 | - name: Lint with flake8
33 | run: |
34 | # stop the build if there are Python syntax errors or undefined names
35 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
36 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
37 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
38 | - name: Test with pytest
39 | run: |
40 | pytest
41 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Testing
2 | testing/
3 |
4 | # Byte-compiled / optimized / DLL files
5 | __pycache__/
6 | *.py[cod]
7 | *$py.class
8 |
9 | # C extensions
10 | *.so
11 |
12 | # Distribution / packaging
13 | .Python
14 | build/
15 | develop-eggs/
16 | dist/
17 | downloads/
18 | eggs/
19 | .eggs/
20 | lib/
21 | lib64/
22 | parts/
23 | sdist/
24 | var/
25 | wheels/
26 | share/python-wheels/
27 | *.egg-info/
28 | .installed.cfg
29 | *.egg
30 | MANIFEST
31 |
32 | # PyInstaller
33 | # Usually these files are written by a python script from a template
34 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
35 | *.manifest
36 | *.spec
37 |
38 | # Installer logs
39 | pip-log.txt
40 | pip-delete-this-directory.txt
41 |
42 | # Unit test / coverage reports
43 | htmlcov/
44 | .tox/
45 | .nox/
46 | .coverage
47 | .coverage.*
48 | .cache
49 | nosetests.xml
50 | coverage.xml
51 | *.cover
52 | *.py,cover
53 | .hypothesis/
54 | .pytest_cache/
55 | cover/
56 | *config/
57 |
58 | # Translations
59 | *.mo
60 | *.pot
61 |
62 | # Django stuff:
63 | *.log
64 | local_settings.py
65 | db.sqlite3
66 | db.sqlite3-journal
67 |
68 | # Flask stuff:
69 | instance/
70 | .webassets-cache
71 |
72 | # Scrapy stuff:
73 | .scrapy
74 |
75 | # Sphinx documentation
76 | docs/_build/
77 |
78 | # PyBuilder
79 | .pybuilder/
80 | target/
81 |
82 | # Jupyter Notebook
83 | .ipynb_checkpoints
84 |
85 | # IPython
86 | profile_default/
87 | ipython_config.py
88 |
89 | # pyenv
90 | # For a library or package, you might want to ignore these files since the code is
91 | # intended to run in multiple environments; otherwise, check them in:
92 | # .python-version
93 |
94 | # pipenv
95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
98 | # install all needed dependencies.
99 | #Pipfile.lock
100 |
101 | # poetry
102 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
103 | # This is especially recommended for binary packages to ensure reproducibility, and is more
104 | # commonly ignored for libraries.
105 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
106 | #poetry.lock
107 |
108 | # pdm
109 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
110 | #pdm.lock
111 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
112 | # in version control.
113 | # https://pdm.fming.dev/#use-with-ide
114 | .pdm.toml
115 |
116 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
117 | __pypackages__/
118 |
119 | # Celery stuff
120 | celerybeat-schedule
121 | celerybeat.pid
122 |
123 | # SageMath parsed files
124 | *.sage.py
125 |
126 | # Environments
127 | .env
128 | .venv
129 | env/
130 | venv*/
131 | ENV/
132 | env.bak/
133 | venv.bak/
134 |
135 | # Spyder project settings
136 | .spyderproject
137 | .spyproject
138 |
139 | # Rope project settings
140 | .ropeproject
141 |
142 | # mkdocs documentation
143 | /site
144 |
145 | # mypy
146 | .mypy_cache/
147 | .dmypy.json
148 | dmypy.json
149 |
150 | # Pyre type checker
151 | .pyre/
152 |
153 | # pytype static type analyzer
154 | .pytype/
155 |
156 | # Cython debug symbols
157 | cython_debug/
158 |
159 | # PyCharm
160 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
161 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
162 | # and can be added to the global gitignore or merged into this file. For a more nuclear
163 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
164 | #.idea/
165 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog],
6 | and this project adheres to [Semantic Versioning].
7 |
8 | ## [Unreleased]
9 |
10 | - /
11 |
12 | ## [1.3.0] - 2024-07-14
13 |
14 | ### Added
15 |
16 | Added new method `get_envs` that returns all the environment variables set in a dict.
17 | ```python
18 | >>> settings.get_envs()
19 | {'PREFIX_APP_IP': '0.0.0.0'}
20 | ```
21 |
22 | ### Fixed
23 |
24 | Fixed docstring indentation.
25 |
26 | ## [1.2.2] - 2024-03-10
27 |
28 | ## New Release v.1.2.2
29 |
30 | ### Fixes
31 |
32 | Fixes `TypeError: unsupported operand type(s) for |: 'type' and 'type'.` error when using Python version < 3.10
33 |
34 | ## [1.2.1] - 2024-02-04
35 |
36 | ## New Release: v1.2.1 - Set environment variables from configuration.
37 |
38 | ### What's New
39 |
40 | Environment variable are now automatically set from the configuration. This makes it easier to use the configuration values in your application.
41 | Env variables of all config keys are set as uppercase. e.g. `APP_HOST` and `APP_PORT` or `APP_CONFIG_APP_HOST` and `APP_CONFIG_APP_PORT` if `env_prefix` is set to "app_config".
42 |
43 | It will also try and convert the values to the correct type. e.g. `APP_PORT` will be set as an integer if the env value is `8080`
44 |
45 | Nested values can also be accessed. ex: `TABLE_KEY_LEVEL1_KEY_LEVEL2_KEY`. This works for any level of nesting.
46 |
47 | Any existing env variables that matches will not be overwritten, but instead will overwrite the existing config value.
48 |
49 | ```python
50 | import os
51 | from simple_toml_configurator import Configuration
52 |
53 | os.environ["PROJECT_APP_PORT"] = "1111"
54 | default = {"app": {"host": "localhost", "port": 8080}}
55 | config = Configuration(
56 | config_path="config_folder",
57 | defaults=default,
58 | config_file_name="app_settings",
59 | env_prefix="project")
60 |
61 | print(os.environ["PROJECT_APP_HOST"]) # Output: 'localhost'
62 | print(os.environ["PROJECT_APP_PORT"]) # Output: '1111'
63 | ```
64 |
65 | ### Fixes
66 |
67 | - Fixed a bug where update_config() would not update the attributes of the Configuration object.
68 |
69 | ## [1.1.0] - 2024-01-28
70 |
71 | ## New Release: v1.1.0 - True Nested Configuration Support with Attribute Access
72 |
73 | This release introduces a significant new feature: Nested Configuration Support with Attribute Access.
74 |
75 | ### What's New
76 |
77 | **Nested Configuration Support with Attribute Access:** In previous versions, accessing and updating nested configuration values required dictionary-style access. With this release, we've made it easier and more intuitive to work with nested configuration values. Now, you can access and update these values using attribute-style access, similar to how you would interact with properties of an object in JavaScript.
78 |
79 | Here's an example:
80 |
81 | ```python
82 | # Access nested configuration values
83 | print(settings.mysql.databases.prod) # Output: 'db1'
84 | settings.mysql.databases.prod = 'new_value'
85 | settings.update()
86 | print(settings.mysql.databases.prod) # Output: 'new_value'
87 | ```
88 |
89 | ## [1.0.0] - 2023-08-27
90 |
91 | - initial release
92 |
93 |
94 | [keep a changelog]: https://keepachangelog.com/en/1.0.0/
95 | [semantic versioning]: https://semver.org/spec/v2.0.0.html
96 |
97 |
98 | [unreleased]: https://github.com/gilbn/simple-toml-configurator/compare/1.3.0...HEAD
99 | [1.3.0]: https://github.com/gilbn/simple-toml-configurator/releases/tag/1.3.0
100 | [1.2.2]: https://github.com/gilbn/simple-toml-configurator/releases/tag/1.2.2
101 | [1.2.1]: https://github.com/gilbn/simple-toml-configurator/releases/tag/1.2.1
102 | [1.1.0]: https://github.com/gilbn/simple-toml-configurator/releases/tag/1.1.0
103 | [1.0.0]: https://github.com/gilbn/simple-toml-configurator/releases/tag/1.0.0
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 GilbN
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Simple TOML Configurator
2 |
3 | [](https://badge.fury.io/py/Simple-TOML-Configurator)
4 | 
5 | 
6 | 
7 | 
8 |
9 | The **Simple TOML Configurator** is a versatile Python library designed to streamline the handling and organization of configuration settings across various types of applications. This library facilitates the management of configuration values through a user-friendly interface and stores these settings in TOML file format for easy readability and accessibility.
10 |
11 | ## Documentation
12 |
13 | https://gilbn.github.io/Simple-TOML-Configurator/
14 |
15 | ## Features
16 |
17 | 1. **Effortless Configuration Management:** The heart of the library is the `Configuration` class, which simplifies the management of configuration settings. It provides intuitive methods to load, update, and store configurations, ensuring a smooth experience for developers.
18 |
19 | 2. **Universal Applicability:** The **Simple TOML Configurator** is designed to work seamlessly with any type of Python application, ranging from web frameworks like Flask, Django, and FastAPI to standalone scripts and command-line utilities.
20 |
21 | 3. **TOML File Storage:** Configuration settings are stored in TOML files, a popular human-readable format. This enables developers to review, modify, and track configuration changes easily.
22 |
23 | 4. **Attribute-Based Access:** Accessing configuration values is straightforward, thanks to the attribute-based approach. Settings can be accessed and updated as attributes, making it convenient for both reading and modifying values.
24 |
25 | 5. **Environment Variable Support:** Configuration values are automatically set as environment variables, making it easier to use the configuration values in your application. Environment variable are set as uppercase. e.g. `APP_HOST` and `APP_PORT` or `PROJECT_APP_HOST` and `PROJECT_APP_PORT` if `env_prefix` is set to "project". This also works for nested values. ex: `TABLE_KEY_LEVEL1_KEY_LEVEL2_KEY`. This works for any level of nesting.**Environment variables set before the configuration is loaded will not be overwritten, but instead will overwrite the existing config value.**
26 |
27 | 6. **Default Values:** Developers can define default values for various configuration sections and keys. The library automatically incorporates new values and manages the removal of outdated ones.
28 |
29 | 7. **Customization Capabilities:** The `Configuration` class can be extended and customized to cater to application-specific requirements. Developers can implement custom logic with getters and setters to handle unique settings or scenarios.
30 |
31 | ## Installation
32 |
33 | Install with
34 | ```bash
35 | pip install simple-toml-configurator
36 | ```
37 |
38 | ## Usage Example
39 |
40 | See [Usage](https://gilbn.github.io/Simple-TOML-Configurator/latest/usage-examples/) for more examples.
41 |
42 | ```python
43 | import os
44 | from simple_toml_configurator import Configuration
45 |
46 | # Define default configuration values
47 | default_config = {
48 | "app": {
49 | "ip": "0.0.0.0",
50 | "host": "",
51 | "port": 5000,
52 | "upload_folder": "uploads",
53 | },
54 | "mysql": {
55 | "user": "root",
56 | "password": "root",
57 | "databases": {
58 | "prod": "db1",
59 | "dev": "db2",
60 | },
61 | }
62 | }
63 |
64 | # Set environment variables
65 | os.environ["PROJECT_APP_UPLOAD_FOLDER"] = "overridden_uploads"
66 |
67 | # Initialize the Simple TOML Configurator
68 | settings = Configuration(config_path="config", defaults=default_config, config_file_name="app_config", env_prefix="project")
69 | # Creates an `app_config.toml` file in the `config` folder at the current working directory.
70 |
71 | # Access and update configuration values
72 | print(settings.app.ip) # Output: '0.0.0.0'
73 | settings.app.ip = "1.2.3.4"
74 | settings.update()
75 | print(settings.app_ip) # Output: '1.2.3.4'
76 |
77 | # Access nested configuration values
78 | print(settings.mysql.databases.prod) # Output: 'db1'
79 | settings.mysql.databases.prod = 'new_value'
80 | settings.update()
81 | print(settings.mysql.databases.prod) # Output: 'new_value'
82 |
83 | # Access and update configuration values
84 | print(settings.app_ip) # Output: '1.2.3.4'
85 | settings.update_config({"app_ip": "1.1.1.1"})
86 | print(settings.app_ip) # Output: '1.1.1.1'
87 |
88 | # Access all settings as a dictionary
89 | all_settings = settings.get_settings()
90 | print(all_settings)
91 | # Output: {'app_ip': '1.1.1.1', 'app_host': '', 'app_port': 5000, 'app_upload_folder': 'overridden_uploads', 'mysql_user': 'root', 'mysql_password': 'root', 'mysql_databases': {'prod': 'new_value', 'dev': 'db2'}}
92 |
93 | # Modify values directly in the config dictionary
94 | settings.config["mysql"]["databases"]["prod"] = "db3"
95 | settings.update()
96 | print(settings.mysql_databases["prod"]) # Output: 'db3'
97 |
98 | # Access environment variables
99 | print(os.environ["PROJECT_MYSQL_DATABASES_PROD"]) # Output: 'db3'
100 | print(os.environ["PROJECT_APP_UPLOAD_FOLDER"]) # Output: 'overridden_uploads'
101 | ```
102 |
103 | **`app_config.toml` contents**
104 |
105 | ```toml
106 | [app]
107 | ip = "1.1.1.1"
108 | host = ""
109 | port = 5000
110 | upload_folder = "overridden_uploads"
111 |
112 | [mysql]
113 | user = "root"
114 | password = "root"
115 |
116 | [mysql.databases]
117 | prod = "db3"
118 | dev = "db2"
119 | ```
120 |
--------------------------------------------------------------------------------
/docs/changelog.md:
--------------------------------------------------------------------------------
1 | --8<-- "CHANGELOG.md"
--------------------------------------------------------------------------------
/docs/configurator.md:
--------------------------------------------------------------------------------
1 | # Simple TOML Configurator Docs
2 | ::: src.simple_toml_configurator.toml_configurator
--------------------------------------------------------------------------------
/docs/css/code_select.css:
--------------------------------------------------------------------------------
1 | .language-pycon .gp, .language-pycon .go { /* Generic.Prompt, Generic.Output */
2 | -webkit-user-select: none;
3 | user-select: none;
4 | }
--------------------------------------------------------------------------------
/docs/exceptions.md:
--------------------------------------------------------------------------------
1 | # Exceptions
2 | ::: src.simple_toml_configurator.exceptions
--------------------------------------------------------------------------------
/docs/flask-custom-example.md:
--------------------------------------------------------------------------------
1 | # Custom Flask Example
2 |
3 | The `Configuration` class can be extended and customized to cater to application-specific requirements. Developers can implement custom logic with getters and setters to handle unique settings or scenarios.
4 |
5 | ## Folder contents
6 |
7 | ```
8 | ├── __init__.py
9 | ├── app.py
10 | ├── utils.py
11 | └── extensions
12 | └── config.py
13 | ```
14 |
15 | This example uses a custom Configuration class in the config.py module that inherits from `Configuration`.
16 |
17 | ??? example "CustomConfiguration"
18 | ```python
19 | class CustomConfiguration(Configuration):
20 | def __init__(self):
21 | super().__init__()
22 |
23 | @property
24 | def logging_debug(self):
25 | return getattr(self, "_logging_debug")
26 |
27 | @logging_debug.setter
28 | def logging_debug(self, value: bool):
29 | if not isinstance(value, bool):
30 | raise ValueError(f"value must be of type bool not {type(value)}")
31 | self._logging_debug = value
32 | log_level = "DEBUG" if value else "INFO"
33 | configure_logging(log_level)
34 | ```
35 |
36 | The custom class uses a property with a setter that executes the `configure_logging` function from `utils.py` whenever the logging_debug attribute is updated. Thus setting the log level to "DEBUG" if the value is True.
37 |
38 | This will change the log level on you Flask app without restarting it.
39 |
40 | ### Code examples
41 |
42 | ??? example "extensions/config.py"
43 | ```python title="config.py"
44 | --8<-- "examples/custom/extensions/config.py"
45 | ```
46 |
47 | ??? example "utils.py"
48 | ```python title="utils.py"
49 | --8<-- "examples/custom/utils.py"
50 | ```
51 |
52 | !!! example "app.py"
53 | ```python title="app.py"
54 | --8<-- "examples/custom/app.py"
55 | ```
--------------------------------------------------------------------------------
/docs/flask-simple-example.md:
--------------------------------------------------------------------------------
1 | # Simple Flask Example
2 |
3 | ## Folder contents
4 |
5 | ```
6 | ├── __init__.py
7 | ├── app.py
8 | └── extensions
9 | └── config.py
10 | ```
11 |
12 | This is a simple example using flask showing how to update the TOML configuration
13 |
14 | ### Code examples
15 |
16 | ??? example "extensions/config.py"
17 | ```python title="config.py"
18 | --8<-- "examples/standard/extensions/config.py"
19 | ```
20 |
21 | !!! example "app.py"
22 | ```python title="app.py"
23 | --8<-- "examples/standard/app.py"
24 | ```
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | --8<-- "README.md"
--------------------------------------------------------------------------------
/docs/installation.md:
--------------------------------------------------------------------------------
1 | ## Installation
2 |
3 | To use the Simple TOML Configurator in your Python projects, you can install it using the `pip` package manager. The package is available on PyPI (Python Package Index).
4 |
5 | ### Prerequisites
6 |
7 | Before installing, make sure you have Python and `pip` installed on your system. You can check if `pip` is installed by running:
8 |
9 | ```bash
10 | pip --version
11 | ```
12 |
13 | If `pip` is not installed, follow the [official `pip` installation guide](https://pip.pypa.io/en/stable/installing/) to get it set up.
14 |
15 | ### Installation Steps
16 |
17 | To install the Simple TOML Configurator package, open your terminal or command prompt and run the following command:
18 |
19 | ```bash
20 | pip install simple-toml-configurator
21 | ```
22 |
23 | This command will download and install the package along with its dependencies. After the installation is complete, you can start using the package in your Python projects.
24 |
25 | See [Usage](usage-examples.md) or [Examples](flask-simple-example.md) for more information on how to use the package.
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
1 | mkdocs>=1.4.0
2 | mkdocstrings[python]>=0.19.0
3 | mkdocs-material>=8.5.6
4 | mkdocs-section-index>=0.3.4
5 | mkdocs-redirects>=1.0.1
6 | mkdocs-git-revision-date-localized-plugin
7 | mkdocs-minify-plugin
8 | pyspelling
9 | mkdocs-include-markdown-plugin>=4.0.4
10 | mkdocs-awesome-pages-plugin>=2.4.0
11 | mkdocs-macros-plugin>=0.4.18
12 | pymdown-extensions>=3.3.2
13 | mike
--------------------------------------------------------------------------------
/docs/usage-examples.md:
--------------------------------------------------------------------------------
1 | ## Usage Examples
2 |
3 | ### Initializing Configuration
4 |
5 | To get started with the Simple TOML Configurator, you need to initialize the configuration settings using the `init_config` method or init `Configuration` object with the arguments. This sets up the default values and configuration file paths.
6 |
7 | ```python
8 | from simple_toml_configurator import Configuration
9 |
10 | # Define default configuration values
11 | default_config = {
12 | "app": {
13 | "ip": "0.0.0.0",
14 | "host": "",
15 | "port": 5000,
16 | "upload_folder": "uploads"
17 | },
18 | "mysql": {
19 | "databases": {
20 | "prod": "db1",
21 | "dev": "db2"
22 | }
23 | }
24 | }
25 |
26 | # Initialize the Simple TOML Configurator
27 | settings = Configuration()
28 | settings.init_config("config", default_config, "app_config")
29 | ```
30 |
31 | #### Load defaults from a TOML file
32 |
33 | ```python
34 | from simple_toml_configurator import Configuration
35 | import tomlkit
36 | import os
37 | from pathlib import Path
38 |
39 | default_file_path = Path(os.path.join(os.getcwd(), "default.toml"))
40 | defaults = tomlkit.loads(file_path.read_text())
41 | settings = Configuration("config", defaults, "app_config")
42 | ```
43 |
44 | ### Accessing Configuration Values
45 |
46 | You can access configuration values as attributes of the `Configuration` instance. This attribute-based access makes it straightforward to retrieve settings.
47 | There are two main ways to access configuration values:
48 |
49 | 1. Attribute access:
50 | - This is the default access method. ex: `settings.app.ip`
51 |
52 | 2. Table prefix access:
53 | - Add the table name as a prefix to the key name. ex: `settings.app_ip` instead of `settings.app.ip`. **This only works for the first level of nesting.**
54 |
55 | !!! info Attribute access
56 | If the table contains a nested table, you can access the nested table using the same syntax. ex: `settings.mysql.databases.prod`
57 |
58 | !!! note
59 | This works for any level of nesting.
60 |
61 | ### Environment variable access
62 |
63 | Access the configuration values as environment variables. ex: `APP_IP` instead of `settings.app.ip`.
64 |
65 | Or `APP_CONFIG_APP_IP` if `env_prefix` is set to "app_config".
66 |
67 | You can also access nested values. ex: `MYSQL_DATABASES_PROD`.
68 |
69 | !!! note
70 | This works for any level of nesting.
71 |
72 | ### Updating Configuration Settings
73 |
74 | Use the `update_config` or `update` method to modify values while ensuring consistency across instances.
75 |
76 | #### update() method
77 |
78 | ```python
79 | # Update the ip key in the app table
80 | settings.app.ip = "1.2.3.4"
81 | settings.update()
82 | ```
83 |
84 | #### update_config() method
85 |
86 | ```python
87 | # Update the ip key in the app table
88 | settings.update_config({"app_ip": "1.2.3.4"})
89 | ```
90 |
91 | #### Update the config dictionary directly
92 |
93 | You can directly modify configuration values within the `config` dictionary. After making changes, use the `update` method to write the updated configuration to the file.
94 |
95 | ```python
96 | # Modify values directly and update configuration
97 | settings.config["app"]["ip"] = "0.0.0.0"
98 | settings.update()
99 | ```
100 |
101 | ### Accessing All Settings
102 |
103 | Retrieve all configuration settings as a dictionary using the `get_settings` method. This provides an overview of all configured values.
104 |
105 | ```python
106 | # Get all configuration settings
107 | all_settings = settings.get_settings()
108 | print(all_settings) # Output: {'app_ip': '1.2.3.4', 'app_host': '', 'app_port': 5000, 'app_upload_folder': 'uploads'}
109 | ```
110 |
111 | ### Customization with Inheritance
112 |
113 | For advanced use cases, you can create a custom configuration class by inheriting from `Configuration`. This allows you to add custom logic and properties tailored to your application.
114 |
115 | ```python
116 | from simple_toml_configurator import Configuration
117 |
118 | class CustomConfiguration(Configuration):
119 | def __init__(self):
120 | super().__init__()
121 |
122 | # Add custom properties with getters and setters
123 | @property
124 | def custom_setting(self):
125 | return getattr(self, "_custom_setting")
126 |
127 | @custom_setting.setter
128 | def custom_setting(self, value):
129 | # Custom logic for updating custom_setting
130 | self._custom_setting = value
131 | # Additional actions can be performed here
132 |
133 | # Initialize and use the custom configuration
134 | custom_settings = CustomConfiguration()
135 | custom_settings.init_config("config", default_config, "custom_config")
136 | ```
--------------------------------------------------------------------------------
/examples/custom/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GilbN/Simple-TOML-Configurator/ed483e7fafafe09fcf3edc41c8f06cc253182554/examples/custom/__init__.py
--------------------------------------------------------------------------------
/examples/custom/app.py:
--------------------------------------------------------------------------------
1 |
2 | from flask import Flask, jsonify, request, url_for,redirect
3 | from extensions.config import settings
4 | import logging
5 |
6 | logger = logging.getLogger(__name__)
7 |
8 | def create_app():
9 | app = Flask(__name__)
10 | app.config["SECRET_KEY"] = settings.config.get("app").get("flask_secret_key")
11 | app.config['APP_PORT'] = settings.config.get("app").get("port")
12 | app.config['APP_IP'] = settings.config.get("app").get("ip")
13 | app.config['APP_HOST'] = settings.config.get("app").get("host")
14 | app.config["DEBUG"] = settings.config.get("app").get("debug")
15 | return app
16 |
17 | app = create_app()
18 |
19 | # simple route that returns the config
20 | @app.route("/")
21 | def app_settings():
22 | return jsonify(
23 | {
24 | "response": {
25 | "data": {
26 | "configuration": settings.get_settings(),
27 | "toml_config": settings.config,
28 | }
29 | }
30 | })
31 |
32 | # Update settings route
33 | @app.route("/update", methods=["POST"])
34 | def update_settings():
35 | """Update settings route"""
36 | data = request.get_json() # {"logging_debug": True, "app_debug": True}
37 | settings.update_config(data)
38 | return redirect(url_for("app_settings"))
39 |
40 | # Get settings value route
41 | @app.route("/logger", methods=["GET"])
42 | def get_settings():
43 | """Sets logging_debug to True or False"""
44 | value = False if settings.logging_debug else True
45 | settings.update_config({"logging_debug": value})
46 | return jsonify({"debug_logging": settings.logging_debug})
47 |
48 | if __name__ == "__main__":
49 | app.run(port=app.config.get("APP_PORT"), host=app.config.get("APP_IP"), debug=app.config.get("APP_DEBUG"))
50 |
--------------------------------------------------------------------------------
/examples/custom/extensions/config.py:
--------------------------------------------------------------------------------
1 |
2 | import logging
3 | import os
4 | from binascii import hexlify
5 | from simple_toml_configurator import Configuration
6 | from utils import configure_logging
7 |
8 | logger = logging.getLogger(__name__)
9 |
10 | default_config = {
11 | "app": {
12 | "ip": "0.0.0.0",
13 | "host": "",
14 | "port": 5000,
15 | "upload_folder": "uploads",
16 | "flask_secret_key": "",
17 | "proxy": "",
18 | "site_url": "http://localhost:5000",
19 | "debug": True,
20 | },
21 | "mysql": {
22 | "host": "",
23 | "port": "",
24 | "user": "",
25 | "password": "",
26 | "databases": {"prod":"test", "dev":"test2"},
27 | },
28 | "scheduler": {
29 | "disabled": True
30 | },
31 | "logging": {
32 | "debug": True
33 | },
34 | "queue": {
35 | "disabled": True
36 | },
37 | }
38 |
39 | config_path = os.environ.get("CONFIG_PATH", os.path.join(os.getcwd(), "config"))
40 |
41 | class CustomConfiguration(Configuration):
42 | def __init__(self):
43 | super().__init__()
44 |
45 | @property
46 | def logging_debug(self):
47 | return getattr(self, "_logging_debug")
48 |
49 | @logging_debug.setter
50 | def logging_debug(self, value: bool):
51 | if not isinstance(value, bool):
52 | raise ValueError(f"value must be of type bool not {type(value)}")
53 | self._logging_debug = value
54 | log_level = "DEBUG" if value else "INFO"
55 | configure_logging(log_level)
56 |
57 | settings = CustomConfiguration()
58 | settings.init_config(config_path, default_config)
59 |
60 | # create random Flask secret_key if there's none in config.toml
61 | if not settings.config.get("app", {}).get("flask_secret_key"):
62 | key = os.environ.get("APP_FLASK_SECRET_KEY", hexlify(os.urandom(16)).decode())
63 | settings.update_config({"app_flask_secret_key": key})
64 |
65 | # Set default mysql host
66 | if not settings.config.get("mysql",{}).get("host"):
67 | mysql_host = os.environ.get("MYSQL_HOST", "localhost")
68 | settings.update_config({"mysql_host": mysql_host})
69 |
70 | # Set default mysql port
71 | if not settings.config.get("mysql",{}).get("port"):
72 | mysql_port = os.environ.get("MYSQL_PORT", "3306")
73 | settings.update_config({"mysql_port": mysql_port})
74 |
75 | # Set default mysql user
76 | if not settings.config.get("mysql",{}).get("user"):
77 | mysql_user = os.environ.get("MYSQL_USER", "root")
78 | settings.update_config({"mysql_user": mysql_user})
79 |
80 | # Set default mysql password
81 | if not settings.config.get("mysql",{}).get("password"):
82 | mysql_password = os.environ.get("MYSQL_PASSWORD", "root")
83 | settings.update_config({"mysql_password": mysql_password})
84 |
85 | if not settings.config.get("mysql",{}).get("database"):
86 | mysql_database = os.environ.get("MYSQL_DATABASE", "some_database")
87 | settings.update_config({"mysql_database": mysql_database})
--------------------------------------------------------------------------------
/examples/custom/utils.py:
--------------------------------------------------------------------------------
1 | import logging
2 | # simple logger function that takes log_level as argument and sets the log level
3 | logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
4 | logger = logging.getLogger()
5 |
6 | def configure_logging(log_level: str) -> None:
7 | """Configure logging for the application
8 |
9 | Args:
10 | log_level (str): Log level to set
11 | """
12 | if logger.getEffectiveLevel() == logging.getLevelName(log_level):
13 | return
14 | logger.setLevel(log_level)
15 | if log_level=="DEBUG":
16 | logger.debug(f"Debug logging enabled")
17 |
--------------------------------------------------------------------------------
/examples/standard/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GilbN/Simple-TOML-Configurator/ed483e7fafafe09fcf3edc41c8f06cc253182554/examples/standard/__init__.py
--------------------------------------------------------------------------------
/examples/standard/app.py:
--------------------------------------------------------------------------------
1 |
2 | from flask import Flask, jsonify, request, url_for,redirect
3 | from extenstions.config import settings
4 |
5 | def create_app():
6 | app = Flask(__name__)
7 | app.config["SECRET_KEY"] = settings.config.get("app").get("flask_secret_key")
8 | app.config['APP_PORT'] = settings.config.get("app").get("port")
9 | app.config['APP_IP'] = settings.config.get("app").get("ip")
10 | app.config['APP_HOST'] = settings.config.get("app").get("host")
11 | app.config["DEBUG"] = settings.config.get("app").get("debug")
12 | return app
13 |
14 | app = create_app()
15 |
16 | # simple route that returns the config
17 | @app.route("/")
18 | def app_settings():
19 | return jsonify(
20 | {
21 | "response": {
22 | "data": {
23 | "configuration": settings.get_settings(),
24 | }
25 | }
26 | })
27 |
28 | # Update settings route
29 | @app.route("/update", methods=["POST"])
30 | def update_settings():
31 | data = request.get_json() # {"app_proxy": "http://localhost:8080", "app_debug": "True}
32 | settings.update_config(data)
33 | return redirect(url_for("app_settings"))
34 |
35 | # Get settings value route
36 | @app.route("/get", methods=["GET"])
37 | def get_settings():
38 | key = request.args.get("key","")
39 | value = request.args.get("value","")
40 | config_attr_value = settings.config.get(key, {}).get(value, "not found")
41 | instance_attr_value = getattr(settings, key, "not found")
42 | return jsonify(
43 | {
44 | "response": {
45 | "data": {
46 | "Configuration.config": {
47 | "key": key,
48 | "value": config_attr_value,
49 | },
50 | "Configuration": {
51 | "key": key,
52 | "value": instance_attr_value,
53 | },
54 | }
55 | }
56 | })
57 |
58 | if __name__ == "__main__":
59 | app.run(port=app.config.get("APP_PORT"), host=app.config.get("APP_IP"), debug=app.config.get("APP_DEBUG"))
60 |
--------------------------------------------------------------------------------
/examples/standard/extensions/config.py:
--------------------------------------------------------------------------------
1 |
2 | import os
3 | from binascii import hexlify
4 | from simple_toml_configurator import Configuration
5 |
6 | default_config = {
7 | "app": {
8 | "ip": "0.0.0.0",
9 | "host": "",
10 | "port": 5000,
11 | "upload_folder": "uploads",
12 | "flask_secret_key": "",
13 | "proxy": "",
14 | "site_url": "http://localhost:5000",
15 | "debug": True,
16 | },
17 | "mysql": {
18 | "host": "",
19 | "port": "",
20 | "user": "",
21 | "password": "",
22 | "databases": {"prod":"test", "dev":"test2"},
23 | },
24 | "scheduler": {
25 | "disabled": True
26 | },
27 | "logging": {
28 | "debug": True
29 | },
30 | "queue": {
31 | "disabled": True
32 | },
33 | }
34 |
35 | config_path = os.environ.get("CONFIG_PATH", os.path.join(os.getcwd(), "config"))
36 | settings = Configuration()
37 | settings.init_config(config_path, default_config)
38 |
39 | # create random Flask secret_key if there's none in config.toml
40 | if not settings.config.get("app", {}).get("flask_secret_key"):
41 | key = os.environ.get("APP_FLASK_SECRET_KEY", hexlify(os.urandom(16)).decode())
42 | settings.update_config({"app_flask_secret_key": key})
43 |
44 | # Set default mysql host
45 | if not settings.config.get("mysql",{}).get("host"):
46 | mysql_host = os.environ.get("MYSQL_HOST", "localhost")
47 | settings.update_config({"mysql_host": mysql_host})
48 |
49 | # Set default mysql port
50 | if not settings.config.get("mysql",{}).get("port"):
51 | mysql_port = os.environ.get("MYSQL_PORT", "3306")
52 | settings.update_config({"mysql_port": mysql_port})
53 |
54 | # Set default mysql user
55 | if not settings.config.get("mysql",{}).get("user"):
56 | mysql_user = os.environ.get("MYSQL_USER", "root")
57 | settings.update_config({"mysql_user": mysql_user})
58 |
59 | # Set default mysql password
60 | if not settings.config.get("mysql",{}).get("password"):
61 | mysql_password = os.environ.get("MYSQL_PASSWORD", "root")
62 | settings.update_config({"mysql_password": mysql_password})
63 |
64 | if not settings.config.get("mysql",{}).get("database"):
65 | mysql_database = os.environ.get("MYSQL_DATABASE", "some_database")
66 | settings.update_config({"mysql_database": mysql_database})
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: Simple TOML Configurator Docs
2 | site_description: ""
3 |
4 | repo_url: https://github.com/GilbN/Simple-TOML-Configurator
5 | repo_name: GilbN/Simple-TOML-Configurator
6 | edit_uri: https://github.com/GilbN/Simple-TOML-Configurator/edit/main/docs/
7 |
8 | theme:
9 | name: material
10 | custom_dir: overrides
11 | features:
12 | - content.code.annotate
13 | - navigation.tabs
14 | - navigation.tabs.sticky
15 | - navigation.instant
16 | - navigation.indexes
17 | - navigation.expand
18 | - navigation.footer
19 | icon:
20 | logo: material/book-open-page-variant
21 | palette:
22 | - media: "(prefers-color-scheme: light)"
23 | scheme: default
24 | primary: teal
25 | accent: teal
26 | toggle:
27 | icon: material/brightness-7
28 | name: Switch to dark mode
29 | - media: "(prefers-color-scheme: dark)"
30 | scheme: slate
31 | primary: indigo
32 | accent: indigo
33 | toggle:
34 | icon: fontawesome/brands/github
35 | name: Switch to light mode
36 | font:
37 | text: Roboto
38 | code: Roboto Mono
39 |
40 | plugins:
41 | - mkdocstrings:
42 | - search
43 | - autorefs
44 | - awesome-pages
45 | - section-index
46 | - macros
47 | - include-markdown
48 | - git-revision-date-localized:
49 | fallback_to_build_date: true
50 | - minify:
51 | minify_html: true
52 | - mike:
53 |
54 | watch:
55 | - src/
56 | - CHANGELOG.md
57 | - examples/
58 |
59 | extra_css:
60 | - css/code_select.css
61 |
62 | nav:
63 | - Home: index.md
64 | - Docs:
65 | - Configurator: configurator.md
66 | - Exceptions: exceptions.md
67 | - Examples:
68 | - Flask Simple: flask-simple-example.md
69 | - Flask Custom: flask-custom-example.md
70 | - How-To Guides:
71 | - Installation: installation.md
72 | - Usage: usage-examples.md
73 | - Changelog: changelog.md
74 |
75 | markdown_extensions:
76 | - pymdownx.highlight:
77 | use_pygments: true
78 | pygments_lang_class: true
79 | anchor_linenums: true
80 | - admonition
81 | - pymdownx.superfences:
82 | custom_fences:
83 | - name: mermaid
84 | class: mermaid
85 | format: !!python/name:pymdownx.superfences.fence_code_format
86 | - pymdownx.details
87 | - pymdownx.emoji
88 | - pymdownx.magiclink
89 | - pymdownx.tasklist:
90 | custom_checkbox: true
91 | - pymdownx.snippets
92 | - pymdownx.inlinehilite
93 | - pymdownx.keys
94 | - toc:
95 | permalink: true
96 |
97 | extra:
98 | version:
99 | provider: mike
100 | homepage: https://github.com/GilbN/Simple-TOML-Configurator
101 | social:
102 | - icon: fontawesome/solid/heart
103 | link: "https://github.com/sponsors/GilbN"
104 | name: Donate
105 | - icon: fontawesome/brands/discord
106 | link: "https://docs.theme-park.dev/discord"
107 | name: Discord
108 | - icon: fontawesome/brands/github
109 | link: "https://github.com/gilbn"
110 | name: GitHub
111 |
--------------------------------------------------------------------------------
/overrides/main.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block outdated %}
4 | You're not viewing the latest version.
5 |
6 | Click here to go to latest.
7 |
8 | {% endblock %}
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools", "wheel"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [project]
6 | name = "Simple-TOML-Configurator"
7 | version = "1.3.0"
8 | license = {text = "MIT License"}
9 | authors = [{name = "GilbN", email = "me@gilbn.dev"}]
10 | description = "A simple TOML configurator for Python"
11 | readme = "README.md"
12 | requires-python = ">=3.7.0"
13 | classifiers = [
14 | "License :: OSI Approved :: MIT License",
15 | "Programming Language :: Python",
16 | "Programming Language :: Python :: 3.7",
17 | "Programming Language :: Python :: 3.8",
18 | "Programming Language :: Python :: 3.9",
19 | "Programming Language :: Python :: 3.10",
20 | "Programming Language :: Python :: 3.11",
21 | ]
22 | keywords = ["configuration", "TOML", "settings", "management", "Python"]
23 | dependencies = [
24 | "tomlkit>=0.12.1",
25 | "python-dateutil>=2.8.2"
26 | ]
27 |
28 | [project.urls]
29 | Homepage = "https://github.com/GilbN/Simple-TOML-Configurator"
30 |
31 | [project.optional-dependencies]
32 | dev = ["pytest","pytest-cov","pytest-mock"]
33 |
34 | [tool.setuptools.dynamic]
35 | version = {attr = "simple_toml_configurator.__version__"}
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | # install all requirements from requirements.txt
2 | -r requirements.txt
3 |
4 | # ... and install more
5 | pytest
6 | pytest-cov
7 | pytest-mock
8 | wheel
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | tomlkit>=0.12.1
2 | python-dateutil>=2.8.2
--------------------------------------------------------------------------------
/src/simple_toml_configurator/__init__.py:
--------------------------------------------------------------------------------
1 | from .toml_configurator import Configuration, ConfigObject
2 |
3 | __all__ = ["Configuration"]
4 |
--------------------------------------------------------------------------------
/src/simple_toml_configurator/exceptions.py:
--------------------------------------------------------------------------------
1 | class TOMLConfiguratorException(Exception):
2 | """Base class for all exceptions raised by the TomlConfigurator class."""
3 | pass
4 |
5 | class TOMLConfigUpdateError(TOMLConfiguratorException):
6 | """Raised when an error occurs while updating the TOML configuration file."""
7 | pass
8 |
9 | class TOMLWriteConfigError(TOMLConfiguratorException):
10 | """Raised when an error occurs while writing the TOML configuration file."""
11 | pass
12 |
13 | class TOMLConfigFileNotFound(TOMLConfiguratorException):
14 | """Raised when the TOML configuration file is not found."""
15 | pass
16 |
17 | class TOMLCreateConfigError(TOMLConfiguratorException):
18 | """Raised when an error occurs while creating the TOML configuration file."""
19 | pass
20 |
21 | class TOMLLoadConfigError(TOMLConfiguratorException):
22 | """Raised when an error occurs while loading the TOML configuration file."""
23 | pass
--------------------------------------------------------------------------------
/src/simple_toml_configurator/toml_configurator.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 | from typing import Any
3 | import os
4 | import re
5 | from collections.abc import Mapping
6 | import logging
7 | import ast
8 | from dateutil.parser import parse, ParserError
9 | from pathlib import Path
10 | import tomlkit
11 | from tomlkit import TOMLDocument
12 | from .exceptions import TOMLConfigUpdateError, TOMLWriteConfigError, TOMLCreateConfigError, TOMLLoadConfigError
13 |
14 | __version__ = "1.3.0"
15 |
16 | logger = logging.getLogger(__name__)
17 |
18 | class Configuration:
19 | """Class to set our configuration values we can use around in our app.
20 | The configuration is stored in a toml file as well as on the instance.
21 |
22 | Examples:
23 | ```pycon
24 | >>> default_config = {
25 | "app": {
26 | "ip": "0.0.0.0",
27 | "host": "",
28 | "port": 5000,
29 | "upload_folder": "uploads"
30 | }
31 | }
32 | >>> import os
33 | >>> from simple_toml_configurator import Configuration
34 | >>> settings = Configuration("config", default_config, "app_config")
35 | {'app': {'ip': '0.0.0.0', 'host': '', 'port': 5000, 'upload_folder': 'uploads'}}
36 | # Update the config dict directly
37 | >>> settings.app.ip = "1.1.1.1"
38 | >>> settings.update()
39 | >>> settings.app.ip
40 | '1.1.1.1'
41 | >>> settings.get_settings()
42 | {'app_ip': '1.1.1.1', 'app_host': '', 'app_port': 5000, 'app_upload_folder': 'uploads'}
43 | >>> settings.update_config({"app_ip":"1.2.3.4"})
44 | >>> settings.app_ip
45 | '1.2.3.4'
46 | >>> settings.config.get("app").get("ip")
47 | '1.2.3.4'
48 | >>> settings.config["app"]["ip"] = "0.0.0.0"
49 | >>> settings.update()
50 | >>> settings.app_ip
51 | '0.0.0.0'
52 | >>> print(os.environ.get("APP_UPLOAD_FOLDER"))
53 | 'uploads'
54 | ```
55 |
56 | Attributes:
57 | config (TOMLDocument): TOMLDocument with all config values
58 | config_path (str|Path): Path to config folder
59 | config_file_name (str): Name of the config file
60 | defaults (dict[str,dict]): Dictionary with all default values for your application
61 |
62 | !!! Info
63 | Changing a table name in your `defaults` will remove the old table in config including all keys and values.
64 |
65 | !!! Note
66 | If updating an attribute needs extra logic, make a custom class that inherits from this class and add property attributes with a getter and setter.
67 |
68 | Example:
69 |
70 | ```python
71 | from simple_toml_configurator import Configuration
72 | from utils import configure_logging
73 |
74 | class CustomConfiguration(Configuration):
75 | def __init__(self):
76 | super().__init__()
77 |
78 | @property
79 | def logging_debug(self):
80 | return getattr(self, "_logging_debug")
81 |
82 | @logging_debug.setter
83 | def logging_debug(self, value: bool):
84 | if not isinstance(value, bool):
85 | raise ValueError(f"value must be of type bool not {type(value)}")
86 | self._logging_debug = value
87 | log_level = "DEBUG" if value else "INFO"
88 | configure_logging(log_level)
89 | logger.debug(f"Debug logging set to {value}")
90 |
91 | defaults = {
92 | "logging": {
93 | "debug": False
94 | ...
95 | }
96 |
97 | config_path = os.environ.get("CONFIG_PATH", "config")
98 | settings = CustomConfiguration()
99 | settings.init_config(config_path, defaults, "app_config"))
100 | ```
101 | """
102 |
103 | def __init__(self, *args, **kwargs) -> None:
104 | self.logger = logging.getLogger("Configuration")
105 | if args or kwargs:
106 | self.init_config(*args, **kwargs)
107 |
108 | def __repr__(self) -> str:
109 | return f"Configuration(config_path={getattr(self,'config_path', None)}, defaults={getattr(self, 'defaults',None)}, config_file_name={getattr(self, 'config_file_name', None)})" # pragma: no cover
110 |
111 | def __str__(self) -> str:
112 | return f" File:'{getattr(self, 'config_file_name', None)}' Path:'{getattr(self, '_full_config_path', None)}'" # pragma: no cover
113 |
114 | def init_config(self, config_path:str|Path, defaults:dict[str,dict], config_file_name:str="config",env_prefix:str="") -> TOMLDocument:
115 | """
116 | Creates the config folder and toml file if needed.
117 |
118 | Upon init it will add any new/missing values/tables from `defaults` into the existing TOML config.
119 | Removes any old values/tables from `self.config` that are not in `self.defaults`.
120 |
121 | Sets all config keys as attributes on the class. e.g. `self.table.key`, `self.table_key` and `self._table_key`
122 |
123 | Env variables of all config keys are set as uppercase. e.g. `APP_HOST` and `APP_PORT` or `APP_CONFIG_APP_HOST` and `APP_CONFIG_APP_PORT` if `env_prefix` is set to "app_config".
124 |
125 | Nested tables are set as nested environment variables. e.g. `APP_CONFIG_APP_DATABASES_PROD` and `APP_CONFIG_APP_DATABASES_DEV`.
126 |
127 | Examples:
128 | ```pycon
129 | >>> settings = Configuration()
130 | >>> settings.init_config("config", defaults, "app_config"))
131 | ```
132 |
133 | Args:
134 | config_path (str|Path): Path to config folder
135 | defaults (dict[str,dict]): Dictionary with all default values for your application
136 | config_file_name (str, optional): Name of the config file. Defaults to "config".
137 | env_prefix (str, optional): Prefix for environment variables. Defaults to "".
138 |
139 | Returns:
140 | dict[str,Any]: Returns a TOMLDocument.
141 | """
142 |
143 | if not isinstance(config_path, (str, Path)):
144 | raise TypeError(f"argument config_path must be of type {type(str)} or {type(Path)}, not: {type(config_path)}") # pragma: no cover
145 | if not isinstance(defaults, dict):
146 | raise TypeError(f"argument defaults must be of type {type(dict)}, not: {type(defaults)}") # pragma: no cover
147 | if not isinstance(env_prefix, str):
148 | raise TypeError(f"argument env_prefix must be of type {type(str)}, not: {type(env_prefix)}") # pragma: no cover
149 | if not isinstance(config_file_name, str):
150 | raise TypeError(f"argument config_file_name must be of type {type(str)}, not: {type(config_file_name)}") # pragma: no cover
151 |
152 | self.defaults:dict[str,dict] = defaults
153 | self.config_path:str|Path = config_path
154 | self.config_file_name:str = f"{config_file_name}.toml"
155 | self.env_prefix:str = env_prefix
156 | self._envs: dict[str,Any] = {}
157 | self._full_config_path:str = os.path.join(self.config_path, self.config_file_name)
158 | self.config:TOMLDocument = self._load_config()
159 | self._sync_config_values()
160 | self._set_os_env()
161 | self._set_attributes()
162 | return self.config
163 |
164 | def _sync_config_values(self) -> None:
165 | """Add any new/missing values/tables from self.defaults into the existing TOML config
166 | - If a table is missing from the config, it will be added with the default table.
167 | - If a table is missing from the defaults, it will be removed from the config.
168 | If there is a mismatch in types between the default value and the config value, the config value will be replaced with the default value.
169 |
170 | For example if the default value is a string and the existing config value is a dictionary, the config value will be replaced with the default value.
171 | """
172 | def update_dict(current_dict:dict, default_dict:dict) -> dict:
173 | """Recursively update a dictionary with another dictionary.
174 |
175 | Args:
176 | current_dict (dict): The dictionary to update. Loaded from the config file.
177 | default_dict (dict): The dictionary to update with. Loaded from the defaults.
178 |
179 | Returns:
180 | dict: The updated dictionary
181 | """
182 | for key, value in default_dict.items():
183 | if isinstance(value, Mapping):
184 | if not isinstance(current_dict.get(key, {}), Mapping):
185 | logger.warning("Mismatched types for key '%s': expected dictionary, got %s. Replacing with new dictionary.", key, type(current_dict.get(key))) # pragma: no cover
186 | current_dict[key] = {}
187 | if key not in current_dict:
188 | logger.info("Adding new Table: ('%s')", key) # pragma: no cover
189 | current_dict[key] = update_dict(current_dict.get(key, {}), value)
190 | else:
191 | if key not in current_dict:
192 | logger.info("Adding new Key: ('%s':'***') in table: %s", key, current_dict) # pragma: no cover
193 | current_dict[key] = value
194 | return current_dict
195 |
196 | self.config = update_dict(self.config, self.defaults)
197 | self._write_config_to_file()
198 | self._clear_old_config_values()
199 |
200 | def _clear_old_config_values(self) -> None:
201 | """Remove any old values/tables from self.config that are not in self.defaults
202 | """
203 | def remove_keys(config:dict, defaults:dict) -> None:
204 | # Create a copy of config to iterate over
205 | config_copy = config.copy()
206 |
207 | # Remove keys that are in config but not in defaults
208 | for key in config_copy:
209 | if key not in defaults:
210 | del config[key]
211 | logger.info("Removing Key: ('%s') in Table: ( '%s' )", key, config_copy)
212 | elif isinstance(config[key], Mapping):
213 | remove_keys(config[key], defaults[key])
214 |
215 | remove_keys(self.config, self.defaults)
216 | self._write_config_to_file()
217 |
218 | def get_settings(self) -> dict[str, Any]:
219 | """Get all config key values as a dictionary.
220 |
221 | Dict keys are formatted as: `table_key`:
222 |
223 | Examples:
224 | ```pycon
225 | >>> defaults = {...}
226 | >>> settings = Configuration()
227 | >>> settings.init_config("config", defaults, "app_config"))
228 | >>> settings.get_settings()
229 | {'app_ip': '0.0.0.0', 'app_host': '', 'app_port': 5000, 'app_upload_folder': 'uploads'}
230 | ```
231 |
232 | Returns:
233 | dict[str, Any]: Dictionary with config key values.
234 | """
235 | settings: dict[str, Any] = {}
236 | for table in self.config:
237 | for key, value in self.config[table].items():
238 | settings[f"{table}_{key}"] = value
239 | return settings
240 |
241 | def get_envs(self) -> dict[str, Any]:
242 | """Get all the environment variables we set as a dictionary.
243 |
244 | Examples:
245 | ```pycon
246 | >>> defaults = {...}
247 | >>> settings = Configuration()
248 | >>> settings.init_config("config", defaults, "app_config"))
249 | >>> settings.get_envs()
250 | {'PREFIX_APP_IP': '0.0.0.0'}
251 |
252 | Returns:
253 | dict[str, Any]: Dictionary with all environment variables name as keys and their value.
254 | """
255 | return self._envs
256 |
257 | def _set_attributes(self) -> dict[str, Any]:
258 | """Set all config keys as attributes on the class.
259 |
260 | Two different attributes are set for each key.
261 | _TOMLtable_key: e.g. `_app_host`
262 | TOMLtable_key: e.g. `app_host`
263 |
264 | This makes it so that the instance attributes are updated when the a value in self.config is updated as they reference the same object.
265 |
266 | Returns:
267 | dict[str, Any]: Returns all attributes in a dictionary
268 | """
269 | for table in self.config:
270 | setattr(self,table,ConfigObject(self.config[table]))
271 | for key, value in self.config[table].items():
272 | setattr(self, f"_{table}_{key}", value)
273 | setattr(self, f"{table}_{key}", value)
274 | self._update_os_env(table, key, value)
275 |
276 | def _parse_env_value(self, value:str) -> Any:
277 | """Parse the environment variable value to the correct type.
278 |
279 | Args:
280 | value (str): The value to parse
281 |
282 | Returns:
283 | Any: The parsed value as the correct type or the original value if it could not be parsed.
284 | """
285 | if str(value).lower() == "true":
286 | return True
287 | if str(value).lower() == "false":
288 | return False
289 | try:
290 | parsed_value = ast.literal_eval(value)
291 | if isinstance(parsed_value, (int, float, bool, str, list, dict)):
292 | return parsed_value
293 | except (ValueError, SyntaxError):
294 | pass
295 | try:
296 | return parse(value)
297 | except ParserError:
298 | pass
299 | return str(value)
300 |
301 | def _make_env_name(self, table:str, key:str) -> str:
302 | """Create an environment variable name from the table and key.
303 |
304 | Args:
305 | table (str): The table name
306 | key (str): The key name
307 |
308 | Returns:
309 | str: The environment variable name
310 | """
311 | if self.env_prefix:
312 | return f"{self.env_prefix.upper()}_{table.upper()}_{str(key).upper()}"
313 | return f"{table.upper()}_{str(key).upper()}"
314 |
315 | def _update_os_env(self, table:str, key:str, value:Any) -> None:
316 | """Update the os environment variable with the same name as the config table and key.
317 |
318 | If the value is a dictionary, creates an environment variable for each item in the dictionary,
319 | handling nested dictionaries recursively.
320 |
321 | Args:
322 | table (str): The table name
323 | key (str): The key name
324 | value (Any): The value
325 | """
326 | if isinstance(value, dict):
327 | for subkey, subvalue in value.items():
328 | self._update_os_env(table, f"{key}_{subkey}", subvalue)
329 | else:
330 | env_var = self._make_env_name(table, key)
331 | os.environ[env_var] = str(value)
332 | self._envs[env_var] = value
333 |
334 | def _set_os_env(self) -> None:
335 | """Set all config keys as environment variables.
336 |
337 | The environment variable is set as uppercase. e.g. `APP_HOST` and `APP_PORT` or `APP_CONFIG_APP_HOST` and `APP_CONFIG_APP_PORT` if `env_prefix` is set to "app_config".
338 |
339 | If the environment variable already exists, the config value is updated to the env value and will overwrite any config value.
340 | """
341 | for table in self.config.copy():
342 | for key, value in self.config[table].items():
343 | existing_env = os.environ.get(self._make_env_name(table, key))
344 | if existing_env:
345 | logger.info("Setting Table: ('%s') Key: ('%s') to value from existing environment variable: ('%s')",table, key, existing_env)
346 | self.config[table][key] = self._parse_env_value(existing_env)
347 | continue
348 | self._update_os_env(table, key, value)
349 | self._write_config_to_file()
350 |
351 | def update_config(self, settings: dict[str,Any]) -> None:
352 | """Update all config values from a dictionary, set new attribute values and write the config to file.
353 | Use the same format as `self.get_settings()` returns to update the config.
354 |
355 | Examples:
356 | ```pycon
357 | >>> defaults = {"mysql": {"databases": {"prod":"prod_db1", "dev":"dev_db1"}}}
358 | >>> settings = Configuration()
359 | >>> settings.init_config("config", defaults, "app_config")
360 | >>> settings.update_config({"mysql_databases": {"prod":"prod_db1", "dev":"dev_db2"}})
361 | print(settings.mysql_databases["dev"])
362 | 'dev_db2'
363 | ```
364 |
365 | Args:
366 | settings (dict): Dict with key values
367 | """
368 | if not isinstance(settings, dict):
369 | raise TypeError(f"Argument settings must be of type {type(dict)}, not: {type(settings)}") # pragma: no cover
370 | try:
371 | for table in self.config:
372 | for key, value in settings.items():
373 | table_key = key.split(f"{table}_")[-1]
374 | if self.config.get(table) and table_key in self.config[table].keys():
375 | if self.config[table][table_key] != value:
376 | self.logger.info("Updating TOML Document -> Table: ('%s') Key: ('%s')",table, table_key)
377 | self.config[table][table_key] = value
378 | except Exception as exc: # pragma: no cover
379 | self.logger.exception("Could not update config!")
380 | raise TOMLConfigUpdateError("unable to update config!") from exc
381 | self._write_config_to_file()
382 | self._set_attributes()
383 |
384 | def update(self):
385 | """Write the current config to file.
386 |
387 | Examples:
388 | ```pycon
389 | >>> from simple_toml_configurator import Configuration
390 | >>> settings = Configuration()
391 | >>> defaults = {"mysql": {"databases": {"prod":"prod_db1", "dev":"dev_db1"}}}
392 | >>> settings.init_config("config", defaults, "app_config")
393 | >>> settings.mysql.databases.prod = "prod_db2"
394 | >>> settings.update()
395 | >>> settings.config["mysql"]["databases"]["prod"]
396 | 'prod_db2'
397 | >>> settings.config["mysql"]["databases"]["prod"] = "prod_db3"
398 | >>> settings.update()
399 | >>> settings.mysql_databases["prod"]
400 | 'prod_db3'
401 | ```
402 | """
403 | self._write_config_to_file()
404 | self._set_attributes()
405 |
406 | def _write_config_to_file(self) -> None:
407 | """Update and write the config to file"""
408 | self.logger.debug("Writing config to file")
409 | try:
410 | with Path(self._full_config_path).open("w", encoding="utf-8") as conf:
411 | toml_document = tomlkit.dumps(self.config)
412 | # Use regular expression to replace consecutive empty lines with a single newline
413 | cleaned_toml = re.sub(r'\n{3,}', '\n\n', toml_document)
414 | conf.write(cleaned_toml)
415 | except (OSError,FileNotFoundError,TypeError) as exc: # pragma: no cover
416 | self.logger.exception("Could not write config file!")
417 | raise TOMLWriteConfigError("unable to write config file!") from exc # pragma: no cover
418 | self.config = self._load_config()
419 |
420 | def _load_config(self) -> TOMLDocument:
421 | """Load the config from file and return it as a TOMLDocument"""
422 | try:
423 | return tomlkit.loads(Path(self._full_config_path).read_text())
424 | except FileNotFoundError: # pragma: no cover
425 | self._create_config(self._full_config_path) # Create the config folder and toml file if needed.
426 | try:
427 | return tomlkit.loads(Path(self._full_config_path).read_text())
428 | except Exception as exc:
429 | self.logger.exception("Could not load config file!")
430 | raise TOMLLoadConfigError("unable to load config file!") from exc
431 |
432 | def _create_config(self, config_file_path:str) -> None:
433 | """Create the config folder and toml file.
434 |
435 | Args:
436 | config_file_path (str): Path to the config file
437 | """
438 |
439 | # Check if config path exists
440 | try:
441 | if not os.path.isdir(os.path.dirname(config_file_path)):
442 | os.makedirs(os.path.dirname(config_file_path), exist_ok=True) # pragma: no cover
443 | except OSError as exc: # pragma: no cover
444 | self.logger.exception("Could not create config folder!")
445 | raise TOMLCreateConfigError(f"unable to create config folder: ({os.path.dirname(config_file_path)})") from exc # pragma: no cover
446 | try:
447 | self.logger.debug("Creating config")
448 | with Path(config_file_path).open("w") as conf:
449 | conf.write(tomlkit.dumps(self.defaults))
450 | except OSError as exc: # pragma: no cover
451 | self.logger.exception("Could not create config file!")
452 | raise TOMLCreateConfigError(f"unable to create config file: ({config_file_path})") from exc
453 |
454 | class ConfigObject:
455 | """
456 | Represents a configuration object that wraps a dictionary and provides attribute access.
457 |
458 | Any key in the dictionary can be accessed as an attribute.
459 |
460 | Args:
461 | table (dict): The dictionary representing the configuration.
462 |
463 | Attributes:
464 | _table (dict): The internal dictionary representing the configuration.
465 |
466 | """
467 |
468 | def __init__(self, table: dict):
469 | self._table = table
470 | for key, value in table.items():
471 | if isinstance(value, dict):
472 | self.__dict__[key] = ConfigObject(value)
473 | else:
474 | self.__dict__[key] = value
475 |
476 | def __setattr__(self, __name: str, __value: Any) -> None:
477 | """Update the table value when an attribute is set"""
478 | super().__setattr__(__name, __value)
479 | if __name == "_table":
480 | return
481 | if hasattr(self, "_table") and __name in self._table:
482 | self._table[__name] = __value
483 |
484 | def __repr__(self) -> str:
485 | return f"ConfigObject({self._table})"
486 |
487 | def __str__(self) -> str:
488 | return f" {self._table}"
--------------------------------------------------------------------------------
/tests/test_toml_configurator.py:
--------------------------------------------------------------------------------
1 | import os
2 | from datetime import datetime
3 | import pytest
4 | from pathlib import Path
5 | from tomlkit import TOMLDocument
6 | from simple_toml_configurator import Configuration
7 | from simple_toml_configurator.toml_configurator import ConfigObject
8 |
9 | @pytest.fixture
10 | def tmp_config_file_name() -> str:
11 | yield "config"
12 |
13 | @pytest.fixture
14 | def tmp_config_path(tmp_path) -> str:
15 | yield tmp_path
16 |
17 | @pytest.fixture
18 | def default_config() -> dict[str, dict]:
19 | yield {
20 | "logging": {
21 | "debug": False
22 | }
23 | }
24 |
25 | @pytest.fixture
26 | def config_instance(tmp_config_path,default_config,tmp_config_file_name) -> Configuration:
27 | config = Configuration()
28 | config.init_config(tmp_config_path, default_config, tmp_config_file_name)
29 | yield config
30 |
31 | @pytest.fixture
32 | def config_instance_env_prefix(tmp_config_path,default_config,tmp_config_file_name) -> Configuration:
33 | config = Configuration()
34 | config.init_config(tmp_config_path, default_config, tmp_config_file_name,env_prefix="TEST")
35 | yield config
36 |
37 | def test_init_config(config_instance: Configuration, tmp_config_path: str, tmp_config_file_name: str, default_config: dict[str, dict]):
38 |
39 | assert isinstance(tmp_config_path, (str, Path))
40 | assert isinstance(tmp_config_file_name, str)
41 | assert isinstance(default_config, dict)
42 |
43 | config_instance.init_config(tmp_config_path, default_config, tmp_config_file_name)
44 | assert isinstance(config_instance.config, TOMLDocument)
45 | assert config_instance.config_path == tmp_config_path
46 | assert config_instance.defaults == default_config
47 | assert config_instance.config_file_name == f"{tmp_config_file_name}.toml"
48 |
49 | def test_sync_config_values(config_instance: Configuration):
50 | config_instance.defaults = {
51 | "logging": {
52 | "debug": False,
53 | "level": "info"
54 | }
55 | }
56 | config_instance.config = TOMLDocument()
57 | config_instance._sync_config_values()
58 | assert "logging" in config_instance.config
59 | assert "debug" in config_instance.config["logging"]
60 | assert "level" in config_instance.config["logging"]
61 | assert config_instance.config["logging"]["level"] == "info"
62 |
63 | def test_clear_old_config_values(config_instance: Configuration):
64 | config_instance.defaults = {
65 | "logging": {
66 | "debug": False
67 | }
68 | }
69 | config_instance.config = TOMLDocument()
70 | config_instance.config["logging"] = {
71 | "debug": False,
72 | "level": "info"
73 | }
74 | config_instance._clear_old_config_values()
75 | assert "level" not in config_instance.config["logging"]
76 |
77 | def test_get_settings(config_instance: Configuration):
78 | config_instance.config = TOMLDocument()
79 | config_instance.config["app"] = {
80 | "host": "localhost",
81 | "port": 8080
82 | }
83 | settings = config_instance.get_settings()
84 | assert settings == {"app_host": "localhost", "app_port": 8080}
85 |
86 | def test_set_attributes(config_instance: Configuration):
87 | config_instance.config = TOMLDocument()
88 | config_instance.config["app"] = {
89 | "host": "localhost",
90 | "port": 8080
91 | }
92 | config_instance._set_attributes()
93 | assert hasattr(config_instance, "_app_host")
94 | assert hasattr(config_instance, "app_host")
95 | assert config_instance._app_host == "localhost"
96 | assert config_instance.app_host == "localhost"
97 | assert config_instance.app.host == "localhost"
98 |
99 | def test_update_config(config_instance: Configuration):
100 | config_instance.config = TOMLDocument()
101 | config_instance.config["app"] = {
102 | "host": "localhost",
103 | "port": 8080
104 | }
105 | config_instance.update_config({"app_host": "test_localhost", "app_port": 8888})
106 | assert config_instance.config["app"]["host"] == "test_localhost"
107 | assert config_instance.config["app"]["port"] == 8888
108 | assert config_instance.app_host == "test_localhost"
109 | assert config_instance.app.port == 8888
110 |
111 | def test_write_config_to_file(config_instance: Configuration, default_config: dict[str, dict]):
112 | config_instance.config = TOMLDocument()
113 | config_instance.config = default_config
114 | config_instance._write_config_to_file()
115 | assert config_instance.config == default_config
116 |
117 | def test_load_config(config_instance: Configuration):
118 | config_instance.config = TOMLDocument()
119 | config_instance.config["app"] = {
120 | "host": "localhost",
121 | "port": 8080
122 | }
123 | config_instance._write_config_to_file()
124 | assert config_instance._load_config() == config_instance.config
125 |
126 | def test_create_config(config_instance: Configuration):
127 | config_instance.config = TOMLDocument()
128 | config_instance.defaults = {
129 | "app": {
130 | "host": "local",
131 | "port": 1000
132 | }}
133 | config_instance._create_config(config_instance._full_config_path)
134 | assert os.path.exists(config_instance._full_config_path)
135 | assert config_instance._load_config() == config_instance.defaults
136 |
137 | def test_update(config_instance: Configuration, default_config: dict[str, dict]):
138 | config_instance.config = default_config
139 | config_instance.config["logging"]["debug"] = True
140 | config_instance.update()
141 | assert config_instance.logging_debug is True
142 |
143 | def test_update_attribute(config_instance: Configuration):
144 | config_instance.logging.debug = True
145 | config_instance.update()
146 | assert config_instance.logging_debug is True
147 | assert config_instance.config["logging"]["debug"] is True
148 |
149 | def test_os_environment_variables(tmp_config_path: str, tmp_config_file_name: str):
150 | config = Configuration()
151 | new_default = {"env": {"test": "test"}}
152 | config.init_config(tmp_config_path, new_default, tmp_config_file_name)
153 | assert os.environ.get("ENV_TEST") == "test"
154 |
155 | def test_nested_os_environment_variavles(tmp_config_path: str, tmp_config_file_name: str):
156 | config = Configuration()
157 | new_default = {"env": {"level1": {"level2": {"test": "test"}}}}
158 | config.init_config(tmp_config_path, new_default, tmp_config_file_name)
159 | assert os.environ.get("ENV_LEVEL1_LEVEL2_TEST") == "test"
160 | assert os.environ.get("ENV_LEVEL1_LEVEL2") is None
161 |
162 | def test_os_environment_variables_with_prefix(config_instance_env_prefix: Configuration):
163 | assert os.environ.get("TEST_LOGGING_DEBUG") == "False"
164 | config_instance_env_prefix.logging.debug = True
165 | config_instance_env_prefix.update()
166 | assert os.environ.get("TEST_LOGGING_DEBUG") == "True"
167 |
168 | def test_os_environment_override(default_config: dict[str, dict], tmp_config_path: str, tmp_config_file_name: str):
169 | os.environ["LOGGING_DEBUG"] = "Disabled"
170 | config = Configuration()
171 | config.init_config(tmp_config_path, default_config, tmp_config_file_name)
172 | assert config.logging_debug == "Disabled"
173 | assert config.logging.debug == "Disabled"
174 | assert config.config["logging"]["debug"] == "Disabled"
175 |
176 | @pytest.fixture
177 | def config_object():
178 | config = {
179 | "app": {
180 | "host": "localhost",
181 | "port": 8080
182 | },
183 | "logging": {
184 | "debug": False,
185 | "level": "info"
186 | }
187 | }
188 | return ConfigObject(config)
189 |
190 | def test_config_object_attribute_access(config_object):
191 | assert config_object.app.host == "localhost"
192 | assert config_object.app.port == 8080
193 | assert config_object.logging.debug is False
194 | assert config_object.logging.level == "info"
195 |
196 | def test_config_object_attribute_update(config_object):
197 | config_object.app.host = "test_localhost"
198 | config_object.app.port = 8888
199 | config_object.logging.debug = True
200 | config_object.logging.level = "debug"
201 | assert config_object.app.host == "test_localhost"
202 | assert config_object.app.port == 8888
203 | assert config_object.logging.debug is True
204 | assert config_object.logging.level == "debug"
205 |
206 | def test_parse_env_value_true(config_instance: Configuration):
207 | values = ["true", "True", "TRUE"]
208 | for value in values:
209 | parsed_value = config_instance._parse_env_value(value)
210 | assert parsed_value is True
211 |
212 | def test_parse_env_value_false(config_instance: Configuration):
213 | values = ["false", "False", "FALSE"]
214 | for value in values:
215 | parsed_value = config_instance._parse_env_value(value)
216 | assert parsed_value is False
217 |
218 | def test_parse_env_value_integer(config_instance: Configuration):
219 | values = ["123000","123_000"]
220 | for value in values:
221 | parsed_value = config_instance._parse_env_value(value)
222 | assert parsed_value == 123000
223 |
224 | def test_parse_env_value_float(config_instance: Configuration):
225 | value = "3.14"
226 | parsed_value = config_instance._parse_env_value(value)
227 | assert parsed_value == 3.14
228 |
229 | def test_parse_env_value_string(config_instance: Configuration):
230 | value = "hello"
231 | parsed_value = config_instance._parse_env_value(value)
232 | assert parsed_value == "hello"
233 |
234 | def test_parse_env_value_list(config_instance: Configuration):
235 | value = "[1, 2, 3]"
236 | parsed_value = config_instance._parse_env_value(value)
237 | assert parsed_value == [1, 2, 3]
238 |
239 | def test_parse_env_value_dict(config_instance: Configuration):
240 | value = '{"key": "value"}'
241 | parsed_value = config_instance._parse_env_value(value)
242 | assert parsed_value == {"key": "value"}
243 |
244 | def test_parse_env_value_datetime(config_instance: Configuration):
245 | value = "2022-01-01 00:00:00"
246 | parsed_value = config_instance._parse_env_value(value)
247 | assert isinstance(parsed_value, datetime)
248 | assert parsed_value.year == 2022
249 | assert parsed_value.month == 1
250 | assert parsed_value.day == 1
251 | assert parsed_value.hour == 0
252 | assert parsed_value.minute == 0
253 | assert parsed_value.second == 0
254 |
255 | def test_parse_env_value_invalid(config_instance: Configuration):
256 | value = "normal string"
257 | parsed_value = config_instance._parse_env_value(value)
258 | assert parsed_value == "normal string"
259 |
260 | def test_make_env_name_with_prefix(config_instance: Configuration):
261 | config_instance.env_prefix = "TEST"
262 | table = "logging"
263 | key = "debug"
264 | env_name = config_instance._make_env_name(table, key)
265 | assert env_name == "TEST_LOGGING_DEBUG"
266 |
267 | def test_make_env_name_without_prefix(config_instance: Configuration):
268 | config_instance.env_prefix = None
269 | table = "app"
270 | key = "port"
271 | env_name = config_instance._make_env_name(table, key)
272 | assert env_name == "APP_PORT"
273 |
274 | def test_get_envs(config_instance: Configuration):
275 | config_instance.logging.debug = False
276 | config_instance.update()
277 | assert config_instance.get_envs() == {"LOGGING_DEBUG": False}
278 |
--------------------------------------------------------------------------------