├── test
├── __init__.py
├── test_files
│ ├── test_read_file
│ ├── test_subfolder_1
│ │ ├── test_sub_subfolder_3
│ │ │ ├── test_subfolder_4.json
│ │ │ └── test_subfolder_3.json
│ │ ├── test_subfolder_1.json
│ │ └── test_sub_subfolder_2
│ │ │ └── test_subfolder_2.json
│ ├── test_none_values.json
│ ├── test.env
│ ├── test.yaml
│ ├── test.ini
│ ├── test.custom
│ ├── test.toml
│ ├── test.json
│ ├── test.hcl
│ └── test.xml
└── test_prase_it.py
├── parse_it
├── envvars
│ ├── __init__.py
│ └── envvars.py
├── file
│ ├── __init__.py
│ ├── env.py
│ ├── ini.py
│ ├── hcl.py
│ ├── json.py
│ ├── toml.py
│ ├── yaml.py
│ ├── xml.py
│ └── file_reader.py
├── command_line_args
│ ├── __init__.py
│ └── command_line_args.py
├── type_estimate
│ ├── __init__.py
│ └── type_estimate.py
├── __init__.py
└── parser.py
├── .github
├── FUNDING.yml
├── PULL_REQUEST_TEMPLATE.md
├── dependabot.yml
├── ISSUE_TEMPLATE.md
└── workflows
│ ├── pr_testing_only.yml
│ └── full_ci_cd_workflow.yml
├── CONTRIBUTORS.md
├── requirements.txt
├── CONTRIBUTING
├── setup.py
├── CODE_OF_CONDUCT.md
├── .gitignore
├── LICENSE
├── README.md
└── CHANGELOG.md
/test/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/parse_it/envvars/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/parse_it/file/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/parse_it/command_line_args/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/parse_it/type_estimate/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [naorlivne]
2 |
--------------------------------------------------------------------------------
/test/test_files/test_read_file:
--------------------------------------------------------------------------------
1 | it_reads!
--------------------------------------------------------------------------------
/parse_it/__init__.py:
--------------------------------------------------------------------------------
1 | from .parser import *
2 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | Fixes #
2 |
3 | ## Proposed Changes
4 |
5 | -
6 | -
7 | -
8 |
--------------------------------------------------------------------------------
/test/test_files/test_subfolder_1/test_sub_subfolder_3/test_subfolder_4.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_json_subfolder_2nd_file": true
3 | }
4 |
--------------------------------------------------------------------------------
/test/test_files/test_none_values.json:
--------------------------------------------------------------------------------
1 | {
2 | "empty_string": "",
3 | "none_lower": "none",
4 | "none_upper": "NONE",
5 | "none_mixed": "None",
6 | "null": null
7 | }
8 |
--------------------------------------------------------------------------------
/CONTRIBUTORS.md:
--------------------------------------------------------------------------------
1 | parse_it contributors
2 | ===================
3 |
4 | * **[Naor Livne](https://github.com/naorlivne)**
5 |
6 | * Creator & BDFL
7 | * Original coding
8 | * Design
9 |
--------------------------------------------------------------------------------
/test/test_files/test.env:
--------------------------------------------------------------------------------
1 | file_type=env
2 | test_string=testing
3 | test_bool_true=true
4 | test_bool_false=false
5 | test_int=123
6 | test_float=123.123
7 | test_list=["test1","test2","test3"]
8 | test_env={"test_env_key": "test_env_value"}
9 |
--------------------------------------------------------------------------------
/test/test_files/test.yaml:
--------------------------------------------------------------------------------
1 | file_type: yaml
2 | test_string: testing
3 | test_bool_true: true
4 | test_bool_false: false
5 | test_int: 123
6 | test_float: 123.123
7 | test_list:
8 | - test1
9 | - test2
10 | - test3
11 | test_yaml:
12 | test_yaml_key: test_yaml_value
13 |
--------------------------------------------------------------------------------
/test/test_files/test.ini:
--------------------------------------------------------------------------------
1 | [DEFAULT]
2 | file_type = "ini"
3 | test_string = "testing"
4 | test_bool_true = true
5 | test_bool_false = false
6 | test_int = 123.0
7 | test_float = 123.123
8 | test_list = '["test1", "test2", "test3"]'
9 |
10 | [test_ini]
11 | test_ini_key = "test_ini_value"
12 |
--------------------------------------------------------------------------------
/test/test_files/test.custom:
--------------------------------------------------------------------------------
1 | file_type: custom_yaml_suffix
2 | test_string: testing_custom
3 | test_bool_true: true
4 | test_bool_false: false
5 | test_int: 123
6 | test_float: 123.123
7 | test_list:
8 | - test1
9 | - test2
10 | - test3
11 | test_yaml:
12 | test_yaml_key: custom_test_yaml_value
13 |
--------------------------------------------------------------------------------
/test/test_files/test.toml:
--------------------------------------------------------------------------------
1 | file_type = "toml"
2 | test_string = "testing"
3 | test_bool_true = true
4 | test_bool_false = false
5 | test_int = 123.0
6 | test_float = 123.123
7 | test_list = [
8 | "test1",
9 | "test2",
10 | "test3"
11 | ]
12 |
13 | [test_toml]
14 | test_toml_key = "test_toml_value"
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | certifi==2025.8.3
2 | chardet==5.2.0
3 | configobj==5.0.9
4 | coverage==7.10.6
5 | dpath==2.2.0
6 | idna==3.10
7 | ply==3.11
8 | pyhcl==0.4.5
9 | python-dotenv==1.1.1
10 | PyYAML==6.0.2
11 | requests==2.32.5
12 | six==1.17.0
13 | toml==0.10.2
14 | urllib3==2.5.0
15 | xmltodict==0.15.1
16 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: pip
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | time: "10:00"
8 | open-pull-requests-limit: 10
9 | labels:
10 | - enhancement
11 | - package-ecosystem: "github-actions"
12 | directory: "/"
13 | schedule:
14 | interval: daily
15 | time: "10:00"
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Expected/Wanted Behavior
2 |
3 |
4 | ## Actual Behavior
5 |
6 |
7 | ## Steps to Reproduce the Problem
8 |
9 | 1.
10 | 2.
11 | 3.
12 |
13 | ## Specifications
14 |
15 | Python version:
16 | (3.6 & higher required, lower versions may work but will not be tested against)
17 |
18 | parse_it version:
19 |
20 | OS type & version:
21 |
--------------------------------------------------------------------------------
/test/test_files/test.json:
--------------------------------------------------------------------------------
1 | {
2 | "file_type": "json",
3 | "test_string": "testing",
4 | "test_bool_true": true,
5 | "test_bool_false": false,
6 | "test_int": 123,
7 | "test_float": 123.123,
8 | "test_list": [
9 | "test1",
10 | "test2",
11 | "test3"
12 | ],
13 | "test_json": {
14 | "test_json_key": "test_json_value"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/test/test_files/test.hcl:
--------------------------------------------------------------------------------
1 | file_type = "hcl"
2 | test_string = "testing"
3 | test_bool_true = true
4 | test_bool_false = false
5 | test_int = 123
6 | test_float = 123.123
7 | test_dict = {
8 | "hcl_dict_key" = "hcl_dict_value"
9 | }
10 | test_list = [
11 | "test1",
12 | "test2",
13 | "test3"
14 | ]
15 |
16 | test_hcl "test_hcl_name" {
17 | test_hcl_key = "test_hcl_value"
18 | }
19 |
--------------------------------------------------------------------------------
/test/test_files/test_subfolder_1/test_subfolder_1.json:
--------------------------------------------------------------------------------
1 | {
2 | "file_type": "json",
3 | "test_string": "testing",
4 | "test_bool_true": true,
5 | "test_bool_false": false,
6 | "test_int": 666,
7 | "test_float": 666.123,
8 | "test_list": [
9 | "test40",
10 | "test50",
11 | "test60"
12 | ],
13 | "test_json": {
14 | "test_json_key": "test_json_value"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/test/test_files/test_subfolder_1/test_sub_subfolder_2/test_subfolder_2.json:
--------------------------------------------------------------------------------
1 | {
2 | "file_type": "json",
3 | "test_string": "testing",
4 | "test_bool_true": true,
5 | "test_bool_false": false,
6 | "test_int": 456,
7 | "test_float": 456.123,
8 | "test_list": [
9 | "test4",
10 | "test5",
11 | "test6"
12 | ],
13 | "test_json": {
14 | "test_json_key": "test_json_value"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/parse_it/file/env.py:
--------------------------------------------------------------------------------
1 | from dotenv import dotenv_values
2 |
3 |
4 | def parse_env_file(path_to_env_file: str) -> dict:
5 | """take a path to a env file & returns it as a valid python dict.
6 |
7 | Arguments:
8 | path_to_env_file -- the path of the json file
9 | Returns:
10 | config_file_dict -- dict of the file
11 | """
12 | return dotenv_values(dotenv_path=path_to_env_file)
13 |
--------------------------------------------------------------------------------
/test/test_files/test_subfolder_1/test_sub_subfolder_3/test_subfolder_3.json:
--------------------------------------------------------------------------------
1 | {
2 | "file_type": "json",
3 | "test_string": "testing",
4 | "test_bool_true": true,
5 | "test_bool_false": false,
6 | "test_int": 789,
7 | "test_float": 789.123,
8 | "test_list": [
9 | "test7",
10 | "test8",
11 | "test9"
12 | ],
13 | "test_json": {
14 | "test_json_key": "test_json_value"
15 | },
16 | "test_json_subfolder": true
17 | }
18 |
--------------------------------------------------------------------------------
/parse_it/file/ini.py:
--------------------------------------------------------------------------------
1 | from configobj import ConfigObj
2 |
3 |
4 | def parse_ini_file(path_to_ini_file: str) -> dict:
5 | """take a path to a INI file & returns it as a valid python dict.
6 |
7 | Arguments:
8 | path_to_ini_file -- the path of the ini file
9 | Returns:
10 | config_file_dict -- dict of the file
11 | """
12 | config = ConfigObj(path_to_ini_file)
13 | return dict(config)
14 |
--------------------------------------------------------------------------------
/parse_it/file/hcl.py:
--------------------------------------------------------------------------------
1 | from parse_it.file.file_reader import *
2 | import hcl
3 |
4 |
5 | def parse_hcl_file(path_to_hcl_file: str) -> dict:
6 | """take a path to a HCL file & returns it as a valid python dict.
7 |
8 | Arguments:
9 | path_to_hcl_file -- the path of the hcl file
10 | Returns:
11 | config_file_dict -- dict of the file
12 | """
13 | return hcl.loads(read_file(path_to_hcl_file))
14 |
--------------------------------------------------------------------------------
/parse_it/file/json.py:
--------------------------------------------------------------------------------
1 | from parse_it.file.file_reader import *
2 | import json
3 |
4 |
5 | def parse_json_file(path_to_json_file: str) -> dict:
6 | """take a path to a JSON file & returns it as a valid python dict.
7 |
8 | Arguments:
9 | path_to_json_file -- the path of the json file
10 | Returns:
11 | config_file_dict -- dict of the file
12 | """
13 | return json.loads(read_file(path_to_json_file))
14 |
--------------------------------------------------------------------------------
/parse_it/file/toml.py:
--------------------------------------------------------------------------------
1 | from parse_it.file.file_reader import *
2 | import toml
3 |
4 |
5 | def parse_toml_file(path_to_toml_file: str) -> dict:
6 | """take a path to a TOML file & returns it as a valid python dict.
7 |
8 | Arguments:
9 | path_to_toml_file -- the path of the toml file
10 | Returns:
11 | config_file_dict -- dict of the file
12 | """
13 | return toml.loads(read_file(path_to_toml_file))
14 |
--------------------------------------------------------------------------------
/parse_it/file/yaml.py:
--------------------------------------------------------------------------------
1 | from parse_it.file.file_reader import *
2 | import yaml
3 |
4 |
5 | def parse_yaml_file(path_to_yaml_file: str) -> dict:
6 | """take a path to a YAML file & returns it as a valid python dict.
7 |
8 | Arguments:
9 | path_to_yaml_file -- the path of the yaml file
10 | Returns:
11 | config_file_dict -- dict of the file
12 | """
13 | return yaml.load(read_file(path_to_yaml_file), Loader=yaml.FullLoader)
14 |
--------------------------------------------------------------------------------
/test/test_files/test.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | xml
4 | false
5 | true
6 | 123.123
7 | 123
8 |
9 | test_xml_value
10 |
11 |
12 | test1
13 | test2
14 | test3
15 |
16 | testing
17 |
--------------------------------------------------------------------------------
/parse_it/file/xml.py:
--------------------------------------------------------------------------------
1 | from parse_it.file.file_reader import *
2 | import xmltodict
3 | import json
4 |
5 |
6 | def parse_xml_file(path_to_xml_file: str) -> dict:
7 | """take a path to a XML file & returns it as a valid python dict.
8 |
9 | Arguments:
10 | path_to_xml_file -- the path of the xml file
11 | Returns:
12 | config_file_dict -- dict of the file
13 | """
14 | # the dump & load to/from JSON is to change it from ordered dict to a normal dict
15 | return json.loads(json.dumps(xmltodict.parse(read_file(path_to_xml_file))))
16 |
--------------------------------------------------------------------------------
/.github/workflows/pr_testing_only.yml:
--------------------------------------------------------------------------------
1 | name: PR CI
2 |
3 | on:
4 | pull_request:
5 | branches: [ "master" ]
6 |
7 | jobs:
8 |
9 | testing_job:
10 |
11 | runs-on: ubuntu-latest
12 | strategy:
13 | matrix:
14 | python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ]
15 | container: python:${{ matrix.python-version }}
16 | steps:
17 | - uses: actions/checkout@v5
18 | - name: Install dependencies
19 | run: |
20 | python -m pip install --upgrade pip
21 | pip install flake8 pytest
22 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
23 | - name: Lint with flake8
24 | run: |
25 | # stop the build if there are Python syntax errors or undefined names
26 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
27 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
28 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
29 | - name: Test
30 | run: |
31 | coverage run -m unittest
32 |
--------------------------------------------------------------------------------
/parse_it/command_line_args/command_line_args.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from typing import Optional
3 |
4 |
5 | def read_command_line_arg(argument: str) -> Optional[str]:
6 | """Read an command line argument.
7 |
8 | Arguments:
9 | argument -- name of the cli argument to get the value of, will auto append "--" to it
10 | Returns:
11 | the value of the argument, None if doesn't exist
12 | """
13 | if command_line_arg_defined(argument) is True:
14 | arg_list = sys.argv
15 | key_index = arg_list.index("--" + argument) + 1
16 | reply = arg_list[key_index]
17 | else:
18 | reply = None
19 | return reply
20 |
21 |
22 | def command_line_arg_defined(argument: str) -> bool:
23 | """Check if a command line argument is defined.
24 |
25 | Arguments:
26 | argument -- name of the cli argument to get the value of, will auto append "--" to it
27 | Returns:
28 | True if argument is declared, False otherwise
29 | """
30 |
31 | if "--" + argument in sys.argv:
32 | return True
33 | else:
34 | return False
35 |
36 |
37 | def read_all_cli_args_to_dict() -> dict:
38 | """Returns all cli args (that start with --) key/value pairs as a single dict.
39 |
40 | Returns:
41 | argument_dict -- A dict of all cli arguments key/value pair
42 | """
43 | argument_dict = {}
44 | arg_list = sys.argv
45 | for argument in arg_list:
46 | if argument.startswith("--"):
47 | key_index = arg_list.index(argument) + 1
48 | argument_dict[argument[2:]] = arg_list[key_index]
49 | return argument_dict
50 |
--------------------------------------------------------------------------------
/CONTRIBUTING:
--------------------------------------------------------------------------------
1 | # How to contribute
2 |
3 | Being an open source project means any help offered will be greatly appreciated, below is a quick run through of how you
4 | can help
5 |
6 | ## Getting Started
7 |
8 | * Fork the latest branch of the component you want to contribute to
9 | * Make sure you have a [GitHub account](https://github.com/signup/free)
10 | * Use virtualenv to install all requirements from the requirements.txt file
11 | * Fix the issue you need \ add a feature
12 | * Make sure the unittests pass & that code coverage didn't drop (you add it, you write the unit test for it)
13 | * Create a pull request
14 | * Once merged the next version release will have your changes in it
15 |
16 | ## Design philosophy
17 | parse_it is designed with the following philosophy in mind, any feature\pull requests will be judged against the following:
18 |
19 | * Follow Linux philosophy and have each component do one thing & one thing only.
20 | * Reliability is more important then shiny new features.
21 | * No vendor\cloud lock-in, if it's only available in one cloud it will not be used.
22 |
23 | ### Documentation
24 |
25 | parse_it docs are hosted at the repo docs/ folder, reading them is highly recommended
26 | prior to making any pull requests
27 |
28 | ## What you can help with
29 |
30 | * Documentation - everyone hates them but without them would you be able to figure out how to use parse_it?
31 | * Bug fixes / feature requests - anything off github issues lists
32 | * Submitting tickets - even if you don't have the time\knowledge to fix a bug just opening a github issue about it will greatly help
33 | * Suggesting improvements
34 | * Spreading the word
35 |
36 | ### Summary
37 |
38 | * Your awesome for helping, thanks.
39 |
40 | P.S.
41 | Don't forget to add yourself to to CONTRIBUTORS.md file.
42 |
--------------------------------------------------------------------------------
/parse_it/type_estimate/type_estimate.py:
--------------------------------------------------------------------------------
1 | import ast
2 | from typing import Any
3 | from contextlib import suppress
4 |
5 |
6 | def estimate_type(node: Any, none_values=None) -> Any:
7 | """ Takes any type and return it's value in a type it estimates it to be based on ast.literal_eval & internal logic,
8 | if the result is a list or a dict will recurse to run all internal values as well, in case of problems parsing the
9 | string with ast.literal_eval it will fallback to sticking with the original type
10 |
11 | Arguments:
12 | node -- the string a type estimation is needed for
13 | none_values -- the values that should be converted to `None`
14 | Returns:
15 | node -- the value of the string in the estimated type
16 |
17 | """
18 |
19 | if none_values is None:
20 | none_values = {"", "null", "none"}
21 |
22 | # this is to support XML type estimation as it returns a dict of all strings
23 | if isinstance(node, dict):
24 | node = str(node)
25 |
26 | if isinstance(node, str):
27 | if node.lower() in {"true", "false"}:
28 | node = node.title()
29 | elif node.lower() in none_values:
30 | node = "None"
31 |
32 | with suppress(ValueError, SyntaxError):
33 | node = ast.literal_eval(node)
34 | if isinstance(node, list):
35 | node = [
36 | estimate_type(item, none_values=none_values)
37 | for item in node
38 | ]
39 | if isinstance(node, dict):
40 | node = {
41 | key: estimate_type(value, none_values=none_values)
42 | for key, value in node.items()
43 | }
44 |
45 | return node
46 |
--------------------------------------------------------------------------------
/.github/workflows/full_ci_cd_workflow.yml:
--------------------------------------------------------------------------------
1 | name: push CI CD
2 |
3 | on:
4 | push:
5 | branches: [ "master" ]
6 |
7 | jobs:
8 |
9 | testing_job:
10 |
11 | runs-on: ubuntu-latest
12 | strategy:
13 | matrix:
14 | python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ]
15 | container: python:${{ matrix.python-version }}
16 | steps:
17 | - uses: actions/checkout@v5
18 | - name: Install dependencies
19 | run: |
20 | python -m pip install --upgrade pip
21 | pip install flake8 pytest
22 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
23 | - name: Lint with flake8
24 | run: |
25 | # stop the build if there are Python syntax errors or undefined names
26 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
27 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
28 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
29 | - name: Test with pytest
30 | run: |
31 | coverage run -m unittest
32 | - name: Upload coverage to Codecov
33 | uses: codecov/codecov-action@v4
34 | with:
35 | token: ${{ secrets.CODECOV_TOKEN }}
36 | fail_ci_if_error: true
37 |
38 | build_and_publish_job:
39 |
40 | runs-on: ubuntu-latest
41 | needs: testing_job
42 | steps:
43 | - uses: actions/checkout@v5
44 | - name: Set up Python
45 | uses: actions/setup-python@v6
46 | with:
47 | python-version: '3.x'
48 | - name: Install dependencies
49 | run: |
50 | python -m pip install --upgrade pip
51 | pip install build
52 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
53 | - name: Build package
54 | run: python -m build
55 | - name: Publish package
56 | uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e
57 | with:
58 | user: __token__
59 | password: ${{ secrets.PYPI_API_TOKEN }}
60 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 | from datetime import datetime
3 |
4 |
5 | __author__ = 'Naor Livne'
6 | __author_email__ = 'naorlivne@gmail.com'
7 | __version__ = datetime.now().strftime("%Y.%m.%d.%H.%M")
8 |
9 | # read the README.md file for the long description of the package
10 | with open('README.md') as f:
11 | long_description = f.read()
12 |
13 | # minimum requirement list
14 | requirements = [
15 | "PyYAML",
16 | "toml",
17 | "configobj",
18 | "xmltodict",
19 | "pyhcl",
20 | "python-dotenv",
21 | "dpath"
22 | ]
23 |
24 | # optional requirements, typing is used for support of Python versions 3.4 & lower, note 3.4 and lower is untested
25 | extra_requirements = {
26 | 'typing': ["typing"]
27 | }
28 |
29 | setup(
30 | name='parse_it',
31 | author=__author__,
32 | author_email=__author_email__,
33 | version=__version__,
34 | description="A python library for parsing multiple types of config files, envvars and command line arguments "
35 | "which takes the headache out of setting app configurations.",
36 | long_description=long_description,
37 | long_description_content_type='text/markdown',
38 | packages=find_packages(exclude=['contrib', 'docs', 'tests']),
39 | extras_require=extra_requirements,
40 | scripts=['setup.py'],
41 | license="LGPLv3",
42 | keywords="parse_it parsing parse parser yaml json xml toml ini cfg hcl envvar environment_variable config "
43 | "cli_args command_line_arguments tml yml configuration configuration_file",
44 | url="https://github.com/naorlivne/parse_it",
45 | install_requires=requirements,
46 | classifiers=[
47 | "Development Status :: 5 - Production/Stable",
48 | "Environment :: Other Environment",
49 | "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
50 | "Operating System :: OS Independent",
51 | "Intended Audience :: Developers",
52 | "Intended Audience :: System Administrators",
53 | "Topic :: Software Development :: Libraries :: Python Modules",
54 | "Programming Language :: Python",
55 | "Programming Language :: Python :: 3",
56 | "Programming Language :: Python :: 3.6",
57 | "Programming Language :: Python :: 3.7",
58 | "Programming Language :: Python :: 3.8",
59 | "Programming Language :: Python :: 3.9",
60 | "Programming Language :: Python :: 3.10",
61 | "Programming Language :: Python :: 3.11",
62 | "Programming Language :: Python :: 3.12",
63 | "Programming Language :: Python :: 3.13"
64 | ]
65 | )
66 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team lead at naorlivne gmail com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | ### VirtualEnv template
3 | # Virtualenv
4 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
5 | .Python
6 | [Bb]in
7 | [Ii]nclude
8 | [Ll]ib
9 | [Ll]ib64
10 | [Ll]ocal
11 | [Ss]cripts
12 | pyvenv.cfg
13 | .venv
14 | pip-selfcheck.json
15 | ### JetBrains template
16 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
17 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
18 |
19 | # User-specific stuff
20 | .idea/**/workspace.xml
21 | .idea/**/tasks.xml
22 | .idea/**/usage.statistics.xml
23 | .idea/**/dictionaries
24 | .idea/**/shelf
25 |
26 | # Sensitive or high-churn files
27 | .idea/**/dataSources/
28 | .idea/**/dataSources.ids
29 | .idea/**/dataSources.local.xml
30 | .idea/**/sqlDataSources.xml
31 | .idea/**/dynamic.xml
32 | .idea/**/uiDesigner.xml
33 | .idea/**/dbnavigator.xml
34 |
35 | # Gradle
36 | .idea/**/gradle.xml
37 | .idea/**/libraries
38 |
39 | # Gradle and Maven with auto-import
40 | # When using Gradle or Maven with auto-import, you should exclude module files,
41 | # since they will be recreated, and may cause churn. Uncomment if using
42 | # auto-import.
43 | # .idea/modules.xml
44 | # .idea/*.iml
45 | # .idea/modules
46 |
47 | # CMake
48 | cmake-build-*/
49 |
50 | # Mongo Explorer plugin
51 | .idea/**/mongoSettings.xml
52 |
53 | # File-based project format
54 | *.iws
55 |
56 | # IntelliJ
57 | out/
58 |
59 | # mpeltonen/sbt-idea plugin
60 | .idea_modules/
61 |
62 | # JIRA plugin
63 | atlassian-ide-plugin.xml
64 |
65 | # Cursive Clojure plugin
66 | .idea/replstate.xml
67 |
68 | # Crashlytics plugin (for Android Studio and IntelliJ)
69 | com_crashlytics_export_strings.xml
70 | crashlytics.properties
71 | crashlytics-build.properties
72 | fabric.properties
73 |
74 | # Editor-based Rest Client
75 | .idea/httpRequests
76 | ### Python template
77 | # Byte-compiled / optimized / DLL files
78 | __pycache__/
79 | *.py[cod]
80 | *$py.class
81 |
82 | # C extensions
83 | *.so
84 |
85 | # Distribution / packaging
86 | .Python
87 | build/
88 | develop-eggs/
89 | dist/
90 | downloads/
91 | eggs/
92 | .eggs/
93 | lib/
94 | lib64/
95 | parts/
96 | sdist/
97 | var/
98 | wheels/
99 | *.egg-info/
100 | .installed.cfg
101 | *.egg
102 | MANIFEST
103 |
104 | # PyInstaller
105 | # Usually these files are written by a python script from a template
106 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
107 | *.manifest
108 | *.spec
109 |
110 | # Installer logs
111 | pip-log.txt
112 | pip-delete-this-directory.txt
113 |
114 | # Unit test / coverage reports
115 | htmlcov/
116 | .tox/
117 | .coverage
118 | .coverage.*
119 | .cache
120 | nosetests.xml
121 | coverage.xml
122 | *.cover
123 | .hypothesis/
124 | .pytest_cache/
125 |
126 | # Translations
127 | *.mo
128 | *.pot
129 |
130 | # Django stuff:
131 | *.log
132 | local_settings.py
133 | db.sqlite3
134 |
135 | # Flask stuff:
136 | instance/
137 | .webassets-cache
138 |
139 | # Scrapy stuff:
140 | .scrapy
141 |
142 | # Sphinx documentation
143 | docs/_build/
144 |
145 | # PyBuilder
146 | target/
147 |
148 | # Jupyter Notebook
149 | .ipynb_checkpoints
150 |
151 | # pyenv
152 | .python-version
153 |
154 | # celery beat schedule file
155 | celerybeat-schedule
156 |
157 | # SageMath parsed files
158 | *.sage.py
159 |
160 | # Environments
161 | .env
162 | .venv
163 | env/
164 | venv/
165 | ENV/
166 | env.bak/
167 | venv.bak/
168 |
169 | # Spyder project settings
170 | .spyderproject
171 | .spyproject
172 |
173 | # Rope project settings
174 | .ropeproject
175 |
176 | # mkdocs documentation
177 | /site
178 |
179 | # mypy
180 | .mypy_cache/
181 |
182 | .idea/
183 | .idea/*
184 | .idea/**
185 | venv/
186 | venv/*
187 | venv/**
188 |
--------------------------------------------------------------------------------
/parse_it/file/file_reader.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | from typing import Optional
3 | import os
4 | import warnings
5 |
6 |
7 | def read_file(file_path: str) -> Optional[str]:
8 | """Read a file and returns it's contents (as a string) or None if file does not exist.
9 |
10 | Arguments:
11 | file_path -- the path of the file to be read
12 | Returns:
13 | file_contents -- string of the file contents
14 | """
15 | try:
16 | with open(file_path) as f:
17 | file_contents = f.read()
18 | return file_contents
19 | except FileNotFoundError:
20 | return None
21 |
22 |
23 | def folder_exists(folder_path: str) -> bool:
24 | """Returns True if folder exists, False otherwise.
25 |
26 | Arguments:
27 | folder_path -- the path of the folder to be checked
28 | Returns:
29 | True if folder is dir, False otherwise
30 | """
31 | possible_folder = Path(strip_trailing_slash(folder_path))
32 | return possible_folder.is_dir()
33 |
34 |
35 | def file_or_folder(checked_path: str) -> Optional[str]:
36 | """Returns "file" if the file_path is a file, "folder" if it's a dir & None otherwise.
37 |
38 | Arguments:
39 | checked_path -- the path of the file to be checked if it's a file or a folder
40 | Returns:
41 | "folder" if the path is a folder, "file" if it's a file and None otherwise
42 | """
43 | checked_file_path = Path(strip_trailing_slash(checked_path))
44 | if checked_file_path.is_dir() is True:
45 | return "folder"
46 | elif checked_file_path.is_file() is True:
47 | return "file"
48 | else:
49 | return None
50 |
51 |
52 | def strip_trailing_slash(folder_path: str) -> str:
53 | """if a folder_path ends in a slash strip it & return the path, otherwise just return the path, only edge case is
54 | the root folder (/) which is kept the same.
55 |
56 | Arguments:
57 | folder_path -- the path of the folder to be checked
58 | Returns:
59 | A trailing slash stripped string of folder_path
60 | """
61 | if len(folder_path) > 1 and folder_path.endswith(("/", "\\")):
62 | folder_path = folder_path[:-1]
63 | return folder_path
64 |
65 |
66 | def file_types_in_folder(folder_path: str, file_types_endings: list, recurse: bool = True) -> dict:
67 | """list all the config file types found inside the given folder based on the filename extension
68 |
69 | Arguments:
70 | folder_path -- the path of the folder to be checked
71 | file_types_endings -- list of file types to look for
72 | recurse -- if True (default) will also look in all subfolders
73 | Returns:
74 | config_files_dict -- dict of {file_type: [list_of_file_names_of_said_type]}
75 | """
76 | folder_path = strip_trailing_slash(folder_path)
77 | if folder_exists(folder_path) is False:
78 | warnings.warn("config_location " + folder_path + " does not exist, only envvars & cli args will be used")
79 | config_files_dict = {}
80 | for file_type_ending in file_types_endings:
81 | config_files_dict[file_type_ending] = []
82 | else:
83 | config_files_dict = {}
84 | for file_type_ending in file_types_endings:
85 | config_files_dict[file_type_ending] = []
86 | if recurse is True:
87 | for root, subFolders, files in os.walk(folder_path, topdown=True):
88 | for file in files:
89 | if file.endswith("." + file_type_ending):
90 | if folder_path + "/" in root:
91 | original_root_path = root
92 | if root[0] != "/":
93 | root = root.split(folder_path + "/", 1)[1]
94 | config_files_dict[file_type_ending].append(os.path.join(root, file))
95 | if original_root_path != root:
96 | root = original_root_path
97 | else:
98 | config_files_dict[file_type_ending].append(file)
99 | else:
100 | for file in os.listdir(folder_path):
101 | if os.path.isfile(os.path.join(folder_path, file)) and file.endswith(file_type_ending):
102 | config_files_dict[file_type_ending].append(file)
103 | config_files_dict[file_type_ending].sort()
104 | return config_files_dict
105 |
--------------------------------------------------------------------------------
/parse_it/envvars/envvars.py:
--------------------------------------------------------------------------------
1 | import os
2 | import dpath.util
3 | import dpath.options
4 | from typing import Optional, Union
5 |
6 |
7 | def read_envvar(envvar: str, force_uppercase: bool = True) -> Optional[str]:
8 | """Read an environment variable, if force_uppercase is true will convert all keys
9 | to be UPPERCASE
10 |
11 | Arguments:
12 | envvar -- name of the envvar to get the value of
13 | force_uppercase -- if the envvar key will be forced to be all in UPPERCASE, defaults to True
14 | Returns:
15 | the value of the envvar, None if doesn't exist
16 | """
17 | if force_uppercase is True:
18 | envvar = envvar.upper()
19 | envvar_value = os.getenv(envvar)
20 |
21 | if isinstance(envvar_value, str) is True:
22 | # this weird encode and decode is to avoid some cases where envvar get special characters escaped
23 | envvar_value = envvar_value.encode('latin1').decode('unicode_escape')
24 | return envvar_value
25 |
26 |
27 | def envvar_defined(envvar: str, force_uppercase: bool = True) -> bool:
28 | """Check if an environment variable is defined, if force_uppercase is true will convert all keys
29 | to be UPPERCASE
30 |
31 | Arguments:
32 | envvar -- name of the envvar to get the value of
33 | force_uppercase -- if the envvar key will be forced to be all in UPPERCASE, defaults to True
34 | Returns:
35 | True if envvar is declared, False otherwise
36 | """
37 | if force_uppercase is True:
38 | envvar = envvar.upper()
39 |
40 | if envvar in os.environ:
41 | return True
42 | else:
43 | return False
44 |
45 |
46 | def read_all_envvars_to_dict(force_uppercase: bool = True) -> dict:
47 | """Read all environment variables and return them in a dict form, if force_uppercase is true will convert all keys
48 | to be UPPERCASE
49 |
50 | Arguments:
51 | force_uppercase -- while counter-intuitive in the naming it means that if the environment variable
52 | is uppercase the dict will treat it as the same one as a lowercase one & will return it in
53 | lowercase form (name saved to match all the other uses of said function)
54 | Returns:
55 | envvar_dict -- A dict of all environment variables key/value pairs
56 | """
57 | envvar_dict = {}
58 | for envvar in os.environ:
59 | if force_uppercase is True:
60 | envvar_dict[envvar.lower()] = os.environ.get(envvar)
61 | else:
62 | envvar_dict[envvar] = os.environ.get(envvar)
63 | return envvar_dict
64 |
65 |
66 | def split_envvar(envvar: Union[str, list], value: str, divider: str = "_"):
67 | """Take an envvar & it's value and split it by the divider to a nested dictionary
68 |
69 | Arguments:
70 | envvar -- the envvar key to split into nested dictionary
71 | value -- the bottom most value of the nested envvars keys
72 | divider -- the string letter by which to divide the envvar key by, defaults to "_"
73 | Returns:
74 | envvar_dict -- A dict that is the result of the envvar being split by the divider with the value
75 | appended as the bottom most of the nest key
76 | """
77 | if type(envvar) == str:
78 | envvar_list = envvar.split(divider)
79 | else:
80 | envvar_list = envvar
81 |
82 | if len(envvar_list) > 1:
83 | envvar_dict = {
84 | envvar_list[0]: split_envvar(envvar_list[1:], value, divider)
85 | }
86 | else:
87 | envvar_dict = {
88 | envvar_list[0]: value
89 | }
90 |
91 | return envvar_dict
92 |
93 |
94 | def split_envvar_combained_dict(divider: str = "_", force_uppercase: bool = True):
95 | """Returns a dict of all envvars that has had their keys split by the divider into nested dicts
96 |
97 | Arguments:
98 | divider -- the string letter by which to divide the envvar key by, defaults to "_"
99 | force_uppercase -- if the envvar key will be forced to be all in UPPERCASE, defaults to True
100 | Returns:
101 | envvar_split_dict -- A dict that is the result of all envvars being split by the divider with
102 | the value appended as the bottom most of the nest key
103 | """
104 | envvar_dict = read_all_envvars_to_dict(force_uppercase=force_uppercase)
105 | envvar_split_dict = {}
106 | for envvar_key, envvar_value in envvar_dict.items():
107 | dpath.options.ALLOW_EMPTY_STRING_KEYS=True
108 | temp_split_envvar = split_envvar(envvar_key, envvar_value, divider=divider)
109 | dpath.util.merge(envvar_split_dict, temp_split_envvar)
110 | return envvar_split_dict
111 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # parse_it
2 |
3 | A python library for parsing multiple types of config files, envvars and command line arguments that takes the headache out of setting app configurations.
4 |
5 | Github actions CI unit tests & auto PyPi push status: [](https://github.com/naorlivne/parse_it/actions/workflows/full_ci_cd_workflow.yml)
6 |
7 | Code coverage: [](https://codecov.io/gh/naorlivne/parse_it)
8 |
9 |
10 | # Install
11 |
12 | First install parse_it, for Python 3.6 & higher this is simply done using pip:
13 |
14 | ```bash
15 | # Install from PyPi for Python version 3.6 & higher
16 | pip install parse_it
17 | ```
18 |
19 | If your using a Python 3.4 or older you will require the `typing` backported package as well, this is done with the following optional install:
20 |
21 | ```bash
22 | # Install from PyPi for Python version 3.4 & lower
23 | pip install parse_it[typing]
24 | ```
25 |
26 | # How to use
27 |
28 | ```python
29 |
30 | # Load parse_it
31 | from parse_it import ParseIt
32 |
33 | # Create parse_it object.
34 | parser = ParseIt()
35 |
36 | # Now you can read your configuration values no matter how they are configured (cli args, envvars, json/yaml/etc files)
37 | my_config_key = parser.read_configuration_variable("my_config_key")
38 |
39 | ```
40 |
41 | By default all configuration files will be assumed to be in the workdir but if you want you can also easily set it to look in all subfolders recursively:
42 |
43 | ```python
44 | # Load parse_it
45 | from parse_it import ParseIt
46 |
47 | # cat /etc/my_config_folder/my_inner_conf_folder/my_config.json >>>
48 | #
49 | # {
50 | # "my_int": 123
51 | # }
52 | #
53 |
54 | # Create parse_it object that will look for the config files in the "/etc/my_config_folder" and all of it's subfolders
55 | parser = ParseIt(config_location="/etc/my_config_folder", recurse=True)
56 | my_config_key = parser.read_configuration_variable("my_int")
57 | # my_config_key will now be an int of 123
58 |
59 | ```
60 |
61 | By default parse_it will look for the configuration options in the following order & will return the first one found:
62 |
63 | * `cli_args` - [command line arguments](https://en.wikipedia.org/wiki/Command-line_interface#Arguments) that are passed in the following format ``--key value``
64 | * `env_vars` - [environment variables](https://en.wikipedia.org/wiki/Environment_variable), you can also use `envvars` as an alias for it
65 | * `env` - [.env](https://github.com/theskumar/python-dotenv#usages) formatted files, any file ending with a .env extension in the configuration folder is assumed to be this
66 | * `json` - [JSON](https://en.wikipedia.org/wiki/JSON) formatted files, any file ending with a .json extension in the configuration folder is assumed to be this
67 | * `yaml` - [YAML](https://en.wikipedia.org/wiki/YAML) formatted files, any file ending with a .yaml extension in the configuration folder is assumed to be this
68 | * `yml` - [YAML](https://en.wikipedia.org/wiki/YAML) formatted files, any file ending with a .yml extension in the configuration folder is assumed to be this
69 | * `toml` - [TOML](https://en.wikipedia.org/wiki/TOML) formatted files, any file ending with a .toml extension in the configuration folder is assumed to be this
70 | * `tml` - [TOML](https://en.wikipedia.org/wiki/TOML) formatted files, any file ending with a .tml extension in the configuration folder is assumed to be this
71 | * `hcl` - [HCL](https://github.com/hashicorp/hcl) formatted files, any file ending with a .hcl extension in the configuration folder is assumed to be this
72 | * `tf` - [HCL](https://github.com/hashicorp/hcl) formatted files, any file ending with a .tf extension in the configuration folder is assumed to be this
73 | * `conf` - [INI](https://en.wikipedia.org/wiki/INI_file) formatted files, any file ending with a .conf extension in the configuration folder is assumed to be this
74 | * `cfg` - [INI](https://en.wikipedia.org/wiki/INI_file) formatted files, any file ending with a .cfg extension in the configuration folder is assumed to be this
75 | * `ini` - [INI](https://en.wikipedia.org/wiki/INI_file) formatted files, any file ending with a .ini extension in the configuration folder is assumed to be this
76 | * `xml` - [XML](https://en.wikipedia.org/wiki/XML) formatted files, any file ending with a .xml extension in the configuration folder is assumed to be this
77 | * configuration default value - every configuration value can also optionally be set with a default value
78 | * global default value - the parser object also has a global default value which can be set
79 |
80 | if multiple files of the same type exists in the same folder parse_it will look in all of them in alphabetical order before going to the next type,
81 |
82 | You can decide on using your own custom order of any subset of the above options (default values excluded, they will always be last):
83 |
84 | ```python
85 | # Load parse_it
86 | from parse_it import ParseIt
87 |
88 | # Create parse_it object which will only look for envvars then yaml & yml files then json files
89 | parser = ParseIt(config_type_priority=["env_vars", "yaml", "yml", "json"])
90 |
91 | ```
92 |
93 | The global default value by default is None but if needed it's simple to set it:
94 |
95 | ```python
96 | # Load parse_it
97 | from parse_it import ParseIt
98 |
99 | # Create parse_it object with a custom default value
100 | parser = ParseIt()
101 | my_config_key = parser.read_configuration_variable("my_undeclared_key")
102 | # my_config_key will now be a None
103 |
104 | # Create parse_it object with a custom default value
105 | parser = ParseIt(global_default_value="my_default_value")
106 | my_config_key = parser.read_configuration_variable("my_undeclared_key")
107 | # my_config_key will now be an string of "my_default_value"
108 |
109 | ```
110 |
111 | parse_it will by default attempt to figure out the type of value returned so even in the case of envvars, cli args & INI files you will get strings/dicts/etc:
112 |
113 | ```python
114 | # Load parse_it
115 | from parse_it import ParseIt
116 |
117 | # This is just for the example
118 | import os
119 | os.environ["MY_INT"] = "123"
120 | os.environ["MY_LIST"] = "['first_item', 'second_item', 'third_item']"
121 | os.environ["MY_DICT"] = "{'key': 'value'}"
122 |
123 | # Create parse_it object
124 | parser = ParseIt()
125 | my_config_key = parser.read_configuration_variable("MY_INT")
126 | # my_config_key will now be an string of "123"
127 | my_config_key = parser.read_configuration_variable("MY_LIST")
128 | # my_config_key will now be an list of ['first_item', 'second_item', 'third_item']
129 | my_config_key = parser.read_configuration_variable("MY_DICT")
130 | # my_config_key will now be an dict of {'key': 'value'}
131 |
132 | # you can easily disable the type estimation
133 | parser = ParseIt(type_estimate=False)
134 | my_config_key = parser.read_configuration_variable("MY_INT")
135 | # my_config_key will now be an string of "123"
136 | my_config_key = parser.read_configuration_variable("MY_LIST")
137 | # my_config_key will now be an string of "['first_item', 'second_item', 'third_item']"
138 | my_config_key = parser.read_configuration_variable("MY_DICT")
139 | # my_config_key will now be an string of "{'key': 'value'}"
140 |
141 | ```
142 |
143 | As envvars recommended syntax is to have all keys be UPPERCASE which is diffrent then all the rest of the configuration files parse_it will automatically change any needed config value to be in ALL CAPS when looking at envvars for the matching value but if needed you can of course disable that feature:
144 |
145 | ```python
146 | # Load parse_it
147 | from parse_it import ParseIt
148 |
149 | # This is just for the example
150 | import os
151 | os.environ["MY_STRING"] = "UPPER"
152 | os.environ["my_string"] = "lower"
153 |
154 | # Create parse_it object
155 | parser = ParseIt()
156 | my_config_key = parser.read_configuration_variable("my_string")
157 | # my_config_key will now be an string of "UPPER"
158 |
159 | # disabling force envvar uppercase
160 | parser = ParseIt(force_envvars_uppercase=False)
161 | my_config_key = parser.read_configuration_variable("my_string")
162 | # my_config_key will now be an string of "lower"
163 |
164 | ```
165 |
166 | You can also easily add a prefix to all envvars (note that `force_envvars_uppercase` will also affect the given prefix):
167 |
168 | ```python
169 | # Load parse_it
170 | from parse_it import ParseIt
171 |
172 | # This is just for the example
173 | import os
174 | os.environ["PREFIX_MY_INT"] = "123"
175 |
176 | # add a prefix to all envvars used
177 | parser = ParseIt(envvar_prefix="prefix_")
178 | my_config_key = parser.read_configuration_variable("my_int")
179 | # my_config_key will now be a int of 123
180 |
181 | ```
182 |
183 | You can also set a default value on a per configuration key basis:
184 |
185 | ```python
186 | # Load parse_it
187 | from parse_it import ParseIt
188 |
189 | # get a default value of the key
190 | parser = ParseIt()
191 | my_config_key = parser.read_configuration_variable("my_undeclared_key", default_value="my_value")
192 | # my_config_key will now be a string of "my_value"
193 |
194 | ```
195 |
196 | You can also declare a key to be required (disabled by default) so it will raise a ValueError if not declared by the user anywhere:
197 |
198 | ```python
199 | # Load parse_it
200 | from parse_it import ParseIt
201 |
202 | # will raise an error as the key is not declared anywhere and required is set to True
203 | parser = ParseIt()
204 | my_config_key = parser.read_configuration_variable("my_undeclared_key", required=True)
205 | # Will raise ValueError
206 |
207 | ```
208 |
209 | While generally not a good idea sometimes you can't avoid it and will need to use a custom non standard file suffix, you can add a custom mapping of suffixes to any of the supported file formats as follows (note that `config_type_priority` should also be set to configure the priority of said custom suffix):
210 |
211 | ```python
212 | # Load parse_it
213 | from parse_it import ParseIt
214 |
215 | # Create parse_it object which will only look for envvars then the custom_yaml_suffix then standard yaml & yml files then json files
216 | parser = ParseIt(config_type_priority=["env_vars", "custom_yaml_suffix", "yaml", "yml", "json"], custom_suffix_mapping={"yaml": ["custom_yaml_suffix"]})
217 |
218 | ```
219 |
220 | You might sometimes want to check that the enduser passed to your config a specific type of variable, parse_it allows you to easily check if a value belongs to a given list of types by setting `allowed_types` which will then raise a TypeError if the value type given is not in the list of `allowed_types`, by default this is set to None so no type ensuring takes place:
221 |
222 | ```python
223 | # Load parse_it
224 | from parse_it import ParseIt
225 |
226 | # This is just for the example
227 | import os
228 | os.environ["ONLY_INTGERS_PLEASE"] = "123"
229 |
230 | # Create parse_it object which will only look for envvars then the custom_yaml_suffix then standard yaml & yml files then json files
231 | parser = ParseIt()
232 |
233 | # skips the type ensuring check as it's not set so all types are accepted
234 | my_config_key = parser.read_configuration_variable("only_intgers_please")
235 |
236 | # the type of the variable value is in the list of allowed_types so no errors\warning\etc will be raised
237 | my_config_key = parser.read_configuration_variable("only_intgers_please", allowed_types=[int])
238 |
239 | # will raise a TypeError
240 | my_config_key = parser.read_configuration_variable("only_intgers_please", allowed_types=[str, dict, list, None])
241 |
242 | ```
243 |
244 | Sometimes you'll need a lot of configuration keys to have the same parse_it configuration params, rather then looping over them yourself this can be achieved with the `read_multiple_configuration_variables` function that you will give it a list of the configuration keys you want & will apply the same configuration to all and return you a dict with the key/value of the configurations back.
245 |
246 | ```python
247 | # Load parse_it
248 | from parse_it import ParseIt
249 |
250 | # Create parse_it object.
251 | parser = ParseIt()
252 |
253 | # Read multiple config keys at once, will return {"my_first_config_key": "default_value", "my_second_config_key": "default_value"} in the example below
254 | my_config_key = parser.read_multiple_configuration_variables(["my_first_config_key", "my_second_config_key"], default_value="default_value", required=False, allowed_types=[str, list, dict, int])
255 |
256 | ```
257 |
258 | You can also read a single file rather then a config directory.
259 |
260 | ```python
261 | # Load parse_it
262 | from parse_it import ParseIt
263 |
264 | # cat /etc/my_config_folder/my_config.json >>>
265 | #
266 | # {
267 | # "my_int": 123
268 | # }
269 | #
270 |
271 | # Create parse_it object that will look at a single config file, envvars & cli
272 | parser = ParseIt(config_location="/etc/my_config_folder/my_config.json")
273 | my_config_key = parser.read_configuration_variable("my_int")
274 | # my_config_key will now be an int of 123
275 |
276 | ```
277 |
278 | Another option is to read all configurations from all valid sources into a single dict that will include the combined results of all of them (by combined it means it will return only the highest priority of each found key & will combine different keys from different sources into a single dict), this provides less flexibility then reading the configuration variables one by one and is a tad (but just a tad) slower but for some use cases is simpler to use:
279 |
280 | ```python
281 | # Load parse_it
282 | from parse_it import ParseIt
283 |
284 | # Create parse_it object
285 | parser = ParseIt()
286 |
287 | my_config_dict = parser.read_all_configuration_variables()
288 | # my_config_dict will now be a dict that includes the keys of all valid sources with the values of each being taken only from the highest priority source
289 |
290 | # you can still define the "default_value", "required" & "allowed_types" when reading all configuration variables to a single dict
291 | my_config_dict = parser.read_all_configuration_variables(default_value={"my_key": "my_default_value", "my_other_key": "my_default_value"}, required=["my_required_key","my_other_required_key"], allowed_types={"my_key": [str, list, dict, int], "my_other_key": [str, list, dict, int]})
292 |
293 | ```
294 |
295 | It has also become a common practice to divide envvar keys by a divider character (usually `_`) and nest then as subdicts, this assists in declaring complex dictionaries subkeys with each of them being given it's own key, parse_it supports this option as well by setting the `envvar_divider` variable when declaring the parse_it object (disabled by default):
296 |
297 | ```python
298 | # Load parse_it
299 | from parse_it import ParseIt
300 |
301 | # This is just for the example
302 | import os
303 | os.environ["NEST1_NEST2_NEST3"] = "123"
304 |
305 | # Create parse_it object with an envvar_divider
306 | parser = ParseIt(envvar_divider="_")
307 |
308 | my_config_dict = parser.read_all_configuration_variables()
309 | # my_config_dict will now be a dict that includes the keys of all valid sources with the values of each being taken only from the highest priority source & the envars keys will be turned to nested subdicts.
310 | # my_config_dict will have in it the following dict {"nest1": {"nest2":{"nest3": 123}}}
311 |
312 | ```
313 | You can define which values should be considered None type. Default is `{"", "null", "none"}`
314 |
315 | ```python
316 | # Load parse_it
317 | from parse_it import ParseIt
318 | # This is just for the example
319 | import os
320 | os.environ["my_config_key1"] = "Nothing"
321 | os.environ["my_config_key2"] = "null"
322 | # Create parse_it object that will only consider "Nothing" and "null" as None type ( defaults to {"", "null", "none"})
323 | parser = ParseIt(none_values={"Nothing", "null"})
324 | my_config_key1 = parser.read_configuration_variable("my_config_key1")
325 | my_config_key2 = parser.read_configuration_variable("my_config_key2")
326 | # my_config_key1 and my_config_key2 will now be `None`
327 |
328 | ```
329 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## [Unreleased](https://github.com/naorlivne/parse_it/tree/HEAD)
4 |
5 | [Full Changelog](https://github.com/naorlivne/parse_it/compare/3.6.0...HEAD)
6 |
7 | **Implemented enhancements:**
8 |
9 | - Bump python-dotenv from 0.17.0 to 0.17.1 [\#112](https://github.com/naorlivne/parse_it/pull/112) ([dependabot[bot]](https://github.com/apps/dependabot))
10 | - Bump python-dotenv from 0.16.0 to 0.17.0 [\#110](https://github.com/naorlivne/parse_it/pull/110) ([dependabot[bot]](https://github.com/apps/dependabot))
11 | - Bump python-dotenv from 0.15.0 to 0.16.0 [\#109](https://github.com/naorlivne/parse_it/pull/109) ([dependabot[bot]](https://github.com/apps/dependabot))
12 | - Bump coverage from 5.4 to 5.5 [\#107](https://github.com/naorlivne/parse_it/pull/107) ([dependabot[bot]](https://github.com/apps/dependabot))
13 |
14 | **Closed issues:**
15 |
16 | - Read the config from the remote server [\#111](https://github.com/naorlivne/parse_it/issues/111)
17 |
18 | ## [3.6.0](https://github.com/naorlivne/parse_it/tree/3.6.0) (2021-03-24)
19 |
20 | [Full Changelog](https://github.com/naorlivne/parse_it/compare/3.5.0...3.6.0)
21 |
22 | **Implemented enhancements:**
23 |
24 | - Bump urllib3 from 1.26.3 to 1.26.4 [\#108](https://github.com/naorlivne/parse_it/pull/108) ([dependabot[bot]](https://github.com/apps/dependabot))
25 | - Bump urllib3 from 1.26.2 to 1.26.3 [\#106](https://github.com/naorlivne/parse_it/pull/106) ([dependabot[bot]](https://github.com/apps/dependabot))
26 | - Bump coverage from 5.3.1 to 5.4 [\#105](https://github.com/naorlivne/parse_it/pull/105) ([dependabot[bot]](https://github.com/apps/dependabot))
27 | - Bump pyyaml from 5.3.1 to 5.4.1 [\#104](https://github.com/naorlivne/parse_it/pull/104) ([dependabot[bot]](https://github.com/apps/dependabot))
28 | - Bump coverage from 5.3 to 5.3.1 [\#100](https://github.com/naorlivne/parse_it/pull/100) ([dependabot[bot]](https://github.com/apps/dependabot))
29 | - Bump codecov from 2.1.10 to 2.1.11 [\#99](https://github.com/naorlivne/parse_it/pull/99) ([dependabot[bot]](https://github.com/apps/dependabot))
30 | - Bump requests from 2.25.0 to 2.25.1 [\#98](https://github.com/naorlivne/parse_it/pull/98) ([dependabot[bot]](https://github.com/apps/dependabot))
31 | - Bump chardet from 3.0.4 to 4.0.0 [\#97](https://github.com/naorlivne/parse_it/pull/97) ([dependabot[bot]](https://github.com/apps/dependabot))
32 | - Bump certifi from 2020.11.8 to 2020.12.5 [\#96](https://github.com/naorlivne/parse_it/pull/96) ([dependabot[bot]](https://github.com/apps/dependabot))
33 |
34 | ## [3.5.0](https://github.com/naorlivne/parse_it/tree/3.5.0) (2020-12-04)
35 |
36 | [Full Changelog](https://github.com/naorlivne/parse_it/compare/3.4.0...3.5.0)
37 |
38 | **Implemented enhancements:**
39 |
40 | - Bump urllib3 from 1.26.1 to 1.26.2 [\#94](https://github.com/naorlivne/parse_it/pull/94) ([dependabot[bot]](https://github.com/apps/dependabot))
41 | - Bump requests from 2.24.0 to 2.25.0 [\#93](https://github.com/naorlivne/parse_it/pull/93) ([dependabot[bot]](https://github.com/apps/dependabot))
42 | - Bump urllib3 from 1.25.11 to 1.26.1 [\#92](https://github.com/naorlivne/parse_it/pull/92) ([dependabot[bot]](https://github.com/apps/dependabot))
43 | - Bump certifi from 2020.6.20 to 2020.11.8 [\#90](https://github.com/naorlivne/parse_it/pull/90) ([dependabot[bot]](https://github.com/apps/dependabot))
44 | - Bump toml from 0.10.1 to 0.10.2 [\#89](https://github.com/naorlivne/parse_it/pull/89) ([dependabot[bot]](https://github.com/apps/dependabot))
45 | - Bump python-dotenv from 0.14.0 to 0.15.0 [\#88](https://github.com/naorlivne/parse_it/pull/88) ([dependabot[bot]](https://github.com/apps/dependabot))
46 | - Bump urllib3 from 1.25.10 to 1.25.11 [\#87](https://github.com/naorlivne/parse_it/pull/87) ([dependabot[bot]](https://github.com/apps/dependabot))
47 | - Bump codecov from 2.1.9 to 2.1.10 [\#86](https://github.com/naorlivne/parse_it/pull/86) ([dependabot[bot]](https://github.com/apps/dependabot))
48 | - Bump coverage from 5.2.1 to 5.3 [\#83](https://github.com/naorlivne/parse_it/pull/83) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
49 | - Bump codecov from 2.1.8 to 2.1.9 [\#82](https://github.com/naorlivne/parse_it/pull/82) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
50 | - Bump coverage from 5.2 to 5.2.1 [\#80](https://github.com/naorlivne/parse_it/pull/80) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
51 | - Bump urllib3 from 1.25.9 to 1.25.10 [\#79](https://github.com/naorlivne/parse_it/pull/79) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
52 | - Bump codecov from 2.1.7 to 2.1.8 [\#78](https://github.com/naorlivne/parse_it/pull/78) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
53 | - Bump coverage from 5.1 to 5.2 [\#77](https://github.com/naorlivne/parse_it/pull/77) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
54 | - Bump python-dotenv from 0.13.0 to 0.14.0 [\#76](https://github.com/naorlivne/parse_it/pull/76) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
55 | - Bump idna from 2.9 to 2.10 [\#75](https://github.com/naorlivne/parse_it/pull/75) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
56 | - Bump certifi from 2020.4.5.2 to 2020.6.20 [\#73](https://github.com/naorlivne/parse_it/pull/73) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
57 | - Bump requests from 2.23.0 to 2.24.0 [\#72](https://github.com/naorlivne/parse_it/pull/72) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
58 | - Bump codecov from 2.1.4 to 2.1.7 [\#71](https://github.com/naorlivne/parse_it/pull/71) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
59 | - Bump certifi from 2020.4.5.1 to 2020.4.5.2 [\#69](https://github.com/naorlivne/parse_it/pull/69) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
60 | - Bump codecov from 2.1.3 to 2.1.4 [\#68](https://github.com/naorlivne/parse_it/pull/68) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
61 | - Bump codecov from 2.1.1 to 2.1.3 [\#67](https://github.com/naorlivne/parse_it/pull/67) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
62 | - Bump six from 1.14.0 to 1.15.0 [\#66](https://github.com/naorlivne/parse_it/pull/66) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
63 | - Bump codecov from 2.1.0 to 2.1.1 [\#65](https://github.com/naorlivne/parse_it/pull/65) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
64 | - Bump codecov from 2.0.22 to 2.1.0 [\#64](https://github.com/naorlivne/parse_it/pull/64) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
65 | - Bump toml from 0.10.0 to 0.10.1 [\#63](https://github.com/naorlivne/parse_it/pull/63) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
66 | - Bump pyhcl from 0.4.3 to 0.4.4 [\#62](https://github.com/naorlivne/parse_it/pull/62) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
67 | - Bump python-dotenv from 0.12.0 to 0.13.0 [\#61](https://github.com/naorlivne/parse_it/pull/61) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
68 | - Bump urllib3 from 1.25.8 to 1.25.9 [\#60](https://github.com/naorlivne/parse_it/pull/60) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
69 | - Bump pyhcl from 0.4.2 to 0.4.3 [\#59](https://github.com/naorlivne/parse_it/pull/59) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
70 | - Bump coverage from 5.0.4 to 5.1 [\#58](https://github.com/naorlivne/parse_it/pull/58) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
71 | - Bump certifi from 2019.11.28 to 2020.4.5.1 [\#57](https://github.com/naorlivne/parse_it/pull/57) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
72 | - Bump pyhcl from 0.4.1 to 0.4.2 [\#56](https://github.com/naorlivne/parse_it/pull/56) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
73 | - Bump pyhcl from 0.4.0 to 0.4.1 [\#55](https://github.com/naorlivne/parse_it/pull/55) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
74 | - Bump codecov from 2.0.21 to 2.0.22 [\#54](https://github.com/naorlivne/parse_it/pull/54) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
75 | - Bump pyyaml from 5.3 to 5.3.1 [\#53](https://github.com/naorlivne/parse_it/pull/53) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
76 | - Bump codecov from 2.0.16 to 2.0.21 [\#52](https://github.com/naorlivne/parse_it/pull/52) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
77 | - Bump coverage from 5.0.3 to 5.0.4 [\#51](https://github.com/naorlivne/parse_it/pull/51) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
78 | - Bump python-dotenv from 0.11.0 to 0.12.0 [\#50](https://github.com/naorlivne/parse_it/pull/50) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
79 | - Bump codecov from 2.0.15 to 2.0.16 [\#48](https://github.com/naorlivne/parse_it/pull/48) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
80 | - Bump requests from 2.22.0 to 2.23.0 [\#47](https://github.com/naorlivne/parse_it/pull/47) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
81 | - Bump idna from 2.8 to 2.9 [\#46](https://github.com/naorlivne/parse_it/pull/46) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
82 | - Bump python-dotenv from 0.10.5 to 0.11.0 [\#45](https://github.com/naorlivne/parse_it/pull/45) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
83 |
84 | **Fixed bugs:**
85 |
86 | - Unresolved import when using "from parse\_it import ParseIt" [\#81](https://github.com/naorlivne/parse_it/issues/81)
87 | - Wrong estimation of type [\#70](https://github.com/naorlivne/parse_it/issues/70)
88 |
89 | **Closed issues:**
90 |
91 | - Dependabot couldn't authenticate with https://pypi.python.org/simple/ [\#84](https://github.com/naorlivne/parse_it/issues/84)
92 | - CLI args nested key names [\#49](https://github.com/naorlivne/parse_it/issues/49)
93 |
94 | **Merged pull requests:**
95 |
96 | - Create Dependabot config file [\#85](https://github.com/naorlivne/parse_it/pull/85) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
97 |
98 | ## [3.4.0](https://github.com/naorlivne/parse_it/tree/3.4.0) (2020-01-28)
99 |
100 | [Full Changelog](https://github.com/naorlivne/parse_it/compare/3.3.2...3.4.0)
101 |
102 | **Implemented enhancements:**
103 |
104 | - Bump dpath from 1.5.0 to 2.0.1 [\#44](https://github.com/naorlivne/parse_it/pull/44) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
105 | - Bump urllib3 from 1.25.7 to 1.25.8 [\#43](https://github.com/naorlivne/parse_it/pull/43) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
106 | - Bump python-dotenv from 0.10.3 to 0.10.5 [\#42](https://github.com/naorlivne/parse_it/pull/42) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
107 | - Bump six from 1.13.0 to 1.14.0 [\#41](https://github.com/naorlivne/parse_it/pull/41) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
108 | - Bump coverage from 5.0.2 to 5.0.3 [\#40](https://github.com/naorlivne/parse_it/pull/40) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
109 | - Bump pyhcl from 0.3.15 to 0.4.0 [\#39](https://github.com/naorlivne/parse_it/pull/39) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
110 | - Bump pyyaml from 5.2 to 5.3 [\#38](https://github.com/naorlivne/parse_it/pull/38) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
111 | - Bump coverage from 5.0.1 to 5.0.2 [\#37](https://github.com/naorlivne/parse_it/pull/37) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
112 | - Bump pyhcl from 0.3.13 to 0.3.15 [\#36](https://github.com/naorlivne/parse_it/pull/36) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
113 | - Bump dpath from 1.4.2 to 1.5.0 [\#35](https://github.com/naorlivne/parse_it/pull/35) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
114 | - Bump coverage from 5.0 to 5.0.1 [\#34](https://github.com/naorlivne/parse_it/pull/34) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
115 | - Bump coverage from 4.5.4 to 5.0 [\#33](https://github.com/naorlivne/parse_it/pull/33) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
116 | - Bump pyyaml from 5.1.2 to 5.2 [\#32](https://github.com/naorlivne/parse_it/pull/32) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
117 | - Bump certifi from 2019.9.11 to 2019.11.28 [\#31](https://github.com/naorlivne/parse_it/pull/31) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
118 | - Bump urllib3 from 1.25.6 to 1.25.7 [\#30](https://github.com/naorlivne/parse_it/pull/30) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
119 | - Bump six from 1.12.0 to 1.13.0 [\#29](https://github.com/naorlivne/parse_it/pull/29) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
120 | - Bump pyhcl from 0.3.12 to 0.3.13 [\#28](https://github.com/naorlivne/parse_it/pull/28) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
121 | - Bump urllib3 from 1.25.5 to 1.25.6 [\#27](https://github.com/naorlivne/parse_it/pull/27) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
122 |
123 | **Closed issues:**
124 |
125 | - Add option to define subdicts via envvars [\#25](https://github.com/naorlivne/parse_it/issues/25)
126 |
127 | ## [3.3.2](https://github.com/naorlivne/parse_it/tree/3.3.2) (2019-09-23)
128 |
129 | [Full Changelog](https://github.com/naorlivne/parse_it/compare/3.3.1...3.3.2)
130 |
131 | ## [3.3.1](https://github.com/naorlivne/parse_it/tree/3.3.1) (2019-09-23)
132 |
133 | [Full Changelog](https://github.com/naorlivne/parse_it/compare/3.3.0...3.3.1)
134 |
135 | ## [3.3.0](https://github.com/naorlivne/parse_it/tree/3.3.0) (2019-09-23)
136 |
137 | [Full Changelog](https://github.com/naorlivne/parse_it/compare/3.2.0...3.3.0)
138 |
139 | **Implemented enhancements:**
140 |
141 | - Bump urllib3 from 1.25.3 to 1.25.5 [\#26](https://github.com/naorlivne/parse_it/pull/26) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
142 | - Bump certifi from 2019.6.16 to 2019.9.11 [\#24](https://github.com/naorlivne/parse_it/pull/24) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
143 |
144 | **Closed issues:**
145 |
146 | - add .env file type support [\#23](https://github.com/naorlivne/parse_it/issues/23)
147 |
148 | ## [3.2.0](https://github.com/naorlivne/parse_it/tree/3.2.0) (2019-08-19)
149 |
150 | [Full Changelog](https://github.com/naorlivne/parse_it/compare/3.1.1...3.2.0)
151 |
152 | ## [3.1.1](https://github.com/naorlivne/parse_it/tree/3.1.1) (2019-08-12)
153 |
154 | [Full Changelog](https://github.com/naorlivne/parse_it/compare/3.1.0...3.1.1)
155 |
156 | **Closed issues:**
157 |
158 | - Add a function to read all configurtion paramters from all possible sources into a single dict [\#19](https://github.com/naorlivne/parse_it/issues/19)
159 |
160 | ## [3.1.0](https://github.com/naorlivne/parse_it/tree/3.1.0) (2019-08-12)
161 |
162 | [Full Changelog](https://github.com/naorlivne/parse_it/compare/3.0.0...3.1.0)
163 |
164 | **Closed issues:**
165 |
166 | - Add the option of giving a single config file location rather then a config folder location [\#22](https://github.com/naorlivne/parse_it/issues/22)
167 |
168 | ## [3.0.0](https://github.com/naorlivne/parse_it/tree/3.0.0) (2019-08-06)
169 |
170 | [Full Changelog](https://github.com/naorlivne/parse_it/compare/2.0.0...3.0.0)
171 |
172 | **Implemented enhancements:**
173 |
174 | - Bump pyyaml from 5.1.1 to 5.1.2 [\#21](https://github.com/naorlivne/parse_it/pull/21) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
175 |
176 | ## [2.0.0](https://github.com/naorlivne/parse_it/tree/2.0.0) (2019-07-30)
177 |
178 | [Full Changelog](https://github.com/naorlivne/parse_it/compare/1.1.0...2.0.0)
179 |
180 | **Implemented enhancements:**
181 |
182 | - Bump coverage from 4.5.3 to 4.5.4 [\#20](https://github.com/naorlivne/parse_it/pull/20) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
183 |
184 | ## [1.1.0](https://github.com/naorlivne/parse_it/tree/1.1.0) (2019-07-23)
185 |
186 | [Full Changelog](https://github.com/naorlivne/parse_it/compare/1.0.0...1.1.0)
187 |
188 | ## [1.0.0](https://github.com/naorlivne/parse_it/tree/1.0.0) (2019-07-16)
189 |
190 | [Full Changelog](https://github.com/naorlivne/parse_it/compare/0.9.1...1.0.0)
191 |
192 | ## [0.9.1](https://github.com/naorlivne/parse_it/tree/0.9.1) (2019-07-04)
193 |
194 | [Full Changelog](https://github.com/naorlivne/parse_it/compare/0.9.0...0.9.1)
195 |
196 | ## [0.9.0](https://github.com/naorlivne/parse_it/tree/0.9.0) (2019-07-01)
197 |
198 | [Full Changelog](https://github.com/naorlivne/parse_it/compare/0.8.1...0.9.0)
199 |
200 | **Implemented enhancements:**
201 |
202 | - Bump certifi from 2019.3.9 to 2019.6.16 [\#17](https://github.com/naorlivne/parse_it/pull/17) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
203 | - Bump pyyaml from 5.1 to 5.1.1 [\#15](https://github.com/naorlivne/parse_it/pull/15) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
204 |
205 | ## [0.8.1](https://github.com/naorlivne/parse_it/tree/0.8.1) (2019-06-05)
206 |
207 | [Full Changelog](https://github.com/naorlivne/parse_it/compare/0.8.0...0.8.1)
208 |
209 | ## [0.8.0](https://github.com/naorlivne/parse_it/tree/0.8.0) (2019-06-04)
210 |
211 | [Full Changelog](https://github.com/naorlivne/parse_it/compare/0.7.0...0.8.0)
212 |
213 | **Implemented enhancements:**
214 |
215 | - Add unit tests for CLI args [\#4](https://github.com/naorlivne/parse_it/issues/4)
216 | - Get code coverage to 100% [\#2](https://github.com/naorlivne/parse_it/issues/2)
217 | - Bump urllib3 from 1.25.2 to 1.25.3 [\#14](https://github.com/naorlivne/parse_it/pull/14) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
218 |
219 | ## [0.7.0](https://github.com/naorlivne/parse_it/tree/0.7.0) (2019-05-20)
220 |
221 | [Full Changelog](https://github.com/naorlivne/parse_it/compare/0.6.1...0.7.0)
222 |
223 | ## [0.6.1](https://github.com/naorlivne/parse_it/tree/0.6.1) (2019-05-19)
224 |
225 | [Full Changelog](https://github.com/naorlivne/parse_it/compare/0.6.0...0.6.1)
226 |
227 | ## [0.6.0](https://github.com/naorlivne/parse_it/tree/0.6.0) (2019-05-19)
228 |
229 | [Full Changelog](https://github.com/naorlivne/parse_it/compare/0.5.11...0.6.0)
230 |
231 | **Implemented enhancements:**
232 |
233 | - Add a changelog [\#12](https://github.com/naorlivne/parse_it/issues/12)
234 | - Bump requests from 2.21.0 to 2.22.0 [\#13](https://github.com/naorlivne/parse_it/pull/13) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
235 |
236 | ## [0.5.11](https://github.com/naorlivne/parse_it/tree/0.5.11) (2019-05-14)
237 |
238 | [Full Changelog](https://github.com/naorlivne/parse_it/compare/0.5.10...0.5.11)
239 |
240 | **Implemented enhancements:**
241 |
242 | - please tag releases in git [\#8](https://github.com/naorlivne/parse_it/issues/8)
243 |
244 | ## [0.5.10](https://github.com/naorlivne/parse_it/tree/0.5.10) (2019-05-13)
245 |
246 | [Full Changelog](https://github.com/naorlivne/parse_it/compare/de836aa8e2e9fed3c2ed8bef8e80399c0b97256b...0.5.10)
247 |
248 | **Implemented enhancements:**
249 |
250 | - move github templates to .github/ directory [\#9](https://github.com/naorlivne/parse_it/issues/9)
251 | - add python version specifiers to pypi metadata [\#7](https://github.com/naorlivne/parse_it/issues/7)
252 | - Add recursive flag support to config file location [\#3](https://github.com/naorlivne/parse_it/issues/3)
253 |
254 | **Merged pull requests:**
255 |
256 | - Refactor `type\_estimate.estimate\_type` with more understandable pattern [\#1](https://github.com/naorlivne/parse_it/pull/1) ([isidentical](https://github.com/isidentical))
257 |
258 |
259 |
260 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
261 |
--------------------------------------------------------------------------------
/parse_it/parser.py:
--------------------------------------------------------------------------------
1 | from parse_it.command_line_args.command_line_args import *
2 | from parse_it.envvars.envvars import *
3 | from parse_it.file.yaml import *
4 | from parse_it.file.ini import *
5 | from parse_it.file.json import *
6 | from parse_it.file.env import *
7 | from parse_it.file.hcl import *
8 | from parse_it.file.toml import *
9 | from parse_it.file.xml import *
10 | from parse_it.type_estimate.type_estimate import *
11 | from parse_it.file.file_reader import *
12 | from typing import Any, Tuple, Optional
13 | import warnings
14 |
15 |
16 | class ParseIt:
17 |
18 | def __init__(self, config_type_priority: Optional[list] = None, global_default_value: Any = None,
19 | type_estimate: bool = True, recurse: bool = False, force_envvars_uppercase: bool = True,
20 | config_location: Optional[str] = None, envvar_prefix: Optional[str] = None,
21 | custom_suffix_mapping: Optional[dict] = None, envvar_divider: Optional[str] = None,
22 | none_values: Optional[set] = None):
23 | """configures the object which is used to query all types of configuration inputs available and prioritize them
24 | based on your needs
25 |
26 | Arguments:
27 | config_type_priority -- a list of file types extensions your willing to accept, list order
28 | dictates priority of said file types, default list order is as follow:
29 | [ "cli_args", "envvars", "env", "json", "yaml", "yml", "toml", "tml", "hcl", "tf", "conf",
30 | "cfg", "ini", "xml" ]
31 | in the case of multiple files of same type they are all read and the first one that has the
32 | needed key is the one used.
33 | if no value is returned then the default_value declared at the read_configuration_variable
34 | will be used and if that is not configured then the global_default_value will be used
35 | global_default_value -- defaults to None, see config_type_priority for it's use
36 | type_estimate -- if set to True (True by default) will try to automatically figure out the type
37 | of the returned value on it's own, useful for envvars & ini type files which always return a
38 | string otherwise
39 | recurse -- False by default, if set to True will also look in all subfolders
40 | force_envvars_uppercase -- if set to True (which is the default) will force all envvars keys to
41 | be UPPERCASE
42 | config_location -- the location where the configuration file(s) will be looked for, if None
43 | (default) will look in the current working directory, accepts either a directory which will
44 | then look in all files in that directory (possibly recursivly depanding on the recurse flag)
45 | or if a standard file will look only in said file
46 | envvar_prefix -- will add the given prefix for all envvars if set
47 | custom_suffix_mapping -- a custom dict which will can map custom file suffixes to a file type
48 | envvar_divider -- the divider to split an envvar to nested dicts, if set to None (default) said
49 | nesting is disabled
50 | none_values -- A tuple with values that should be considered `None`, if set to None (default) it
51 | will take the default set tuple of {"", "null", "none"}
52 | """
53 |
54 | # first we describe the standard file type suffix mapping and what file types are are standard file extensions
55 | self.suffix_file_type_mapping = {
56 | "env": [
57 | "env"
58 | ],
59 | "json": [
60 | "json"
61 | ],
62 | "yaml": [
63 | "yaml",
64 | "yml"
65 | ],
66 | "toml": [
67 | "toml",
68 | "tml"
69 | ],
70 | "hcl": [
71 | "hcl",
72 | "tf"
73 | ],
74 | "ini": [
75 | "conf",
76 | "cfg",
77 | "ini"
78 | ],
79 | "xml": [
80 | "xml"
81 | ]
82 | }
83 | self.valid_file_type_extension = [
84 | "env",
85 | "json",
86 | "yaml",
87 | "yml",
88 | "toml",
89 | "tml",
90 | "hcl",
91 | "tf",
92 | "conf",
93 | "cfg",
94 | "ini",
95 | "xml"
96 | ]
97 |
98 | if envvar_divider is None:
99 | self.nest_envvars = False
100 | else:
101 | self.nest_envvars = True
102 | self.envvar_divider = envvar_divider
103 |
104 | # now we add any custom file suffixes the user wanted to the list of possible file extensions and valid
105 | # suffixes
106 | if custom_suffix_mapping is not None:
107 | for file_type, custom_file_suffix in custom_suffix_mapping.items():
108 | self.suffix_file_type_mapping[file_type] = self.suffix_file_type_mapping[file_type] + custom_file_suffix
109 | custom_suffixes_list = [suffix_value for suffix_list in custom_suffix_mapping.values() for suffix_value in
110 | suffix_list]
111 | self.valid_file_type_extension += custom_suffixes_list
112 | if config_type_priority is None:
113 | warnings.warn("custom_suffix_mapping is defined but config_type_priority is using the default setting, "
114 | "custom file suffixes will not be used")
115 |
116 | if envvar_prefix is None:
117 | self.envvar_prefix = ""
118 | else:
119 | self.envvar_prefix = envvar_prefix
120 |
121 | self.global_default_value = global_default_value
122 | self.type_estimate = type_estimate
123 | self.force_envvars_uppercase = force_envvars_uppercase
124 |
125 | if config_type_priority is None:
126 | self.config_type_priority = [
127 | "cli_args",
128 | "env_vars",
129 | "env",
130 | "json",
131 | "yaml",
132 | "yml",
133 | "toml",
134 | "tml",
135 | "hcl",
136 | "tf",
137 | "conf",
138 | "cfg",
139 | "ini",
140 | "xml"
141 | ]
142 | else:
143 | self.config_type_priority = config_type_priority
144 |
145 | if config_location is None:
146 | self.config_location = os.getcwd()
147 | else:
148 | self.config_location = config_location
149 |
150 | # create a list of all valid types
151 | valid_config_types = []
152 | for config_type in self.config_type_priority:
153 | if config_type in self.valid_file_type_extension:
154 | valid_config_types.append(config_type)
155 |
156 | # we check if the config is a file or a folder
157 | self.config_file_type = file_or_folder(self.config_location)
158 |
159 | # if config is dict or not declared populate the config_files_dict with the list of locations for each file type
160 | if self.config_file_type == "folder" or self.config_file_type is None:
161 | self.config_files_dict = file_types_in_folder(self.config_location, valid_config_types,
162 | recurse=recurse)
163 | # if the config is a file populate the config_files_dict with that single file and have the rest file types
164 | # be blank
165 | elif self.config_file_type == "file":
166 | self.config_files_dict = {}
167 | for file_type_ending in valid_config_types:
168 | self.config_files_dict[file_type_ending] = []
169 | if self.config_location.endswith(file_type_ending):
170 | self.config_files_dict[file_type_ending].append(self.config_location)
171 |
172 | if none_values is None:
173 | self.none_values = {"", "null", "none"}
174 | else:
175 | self.none_values = none_values
176 |
177 | def read_configuration_variable(self, config_name: str, default_value: Any = None, required: bool = False,
178 | allowed_types: Optional[list] = None) -> Any:
179 | """reads a single key of the configuration and returns the first value of it found based on the priority of each
180 | config file option given in the __init__ of the class
181 |
182 | Arguments:
183 | config_name -- the configuration key name you want to get the value of
184 | default_value -- defaults to None, see config_type_priority in class init for it's use
185 | required -- defaults to False, if set to True will ignore default_value & global_default_value
186 | and will raise an ValueError if the configuration is not configured in any of the config
187 | files/envvars/cli args
188 | allowed_types -- Defaults to None, an optional list of types that are accepted for the variable
189 | to be, if set a check will be preformed and if the variables value given is not of any of
190 | the types in said list a TypeError will be raised
191 | Returns:
192 | config_value -- the value of the configuration requested
193 | """
194 |
195 | # we start with both key not found and the value being None
196 | config_value = None
197 | config_key_found = False
198 |
199 | # we now loop over all the permitted types of where the config key might be and break on the first one found
200 | # after setting config_key_found to True and config_value to the value found
201 | for config_type in self.config_type_priority:
202 | if config_type == "cli_args":
203 | config_key_found = command_line_arg_defined(config_name)
204 | if config_key_found is True:
205 | config_value = read_command_line_arg(config_name)
206 | break
207 | elif config_type == "envvars" or config_type == "env_vars":
208 | config_key_found = envvar_defined(self.envvar_prefix + config_name,
209 | force_uppercase=self.force_envvars_uppercase)
210 | config_value = read_envvar(self.envvar_prefix + config_name,
211 | force_uppercase=self.force_envvars_uppercase)
212 | if config_key_found is True:
213 | # next the envvar if so desired to assist in matching to other file formats
214 | if self.nest_envvars is True:
215 | config_value = split_envvar(config_name, config_value, divider=self.envvar_divider)
216 | break
217 |
218 | # will loop over all files of each type until all files of all types are searched, first time the key is
219 | # found will break outside of both loops
220 | elif config_type in self.valid_file_type_extension:
221 | for config_file in self.config_files_dict[config_type]:
222 | if self.config_file_type == "file":
223 | file_dict = self._parse_file_per_type(config_type, config_file)
224 | else:
225 | file_dict = self._parse_file_per_type(config_type, os.path.join(self.config_location,
226 | config_file))
227 | config_key_found, config_value = self._check_config_in_dict(config_name, file_dict)
228 | if config_key_found is True:
229 | break
230 | if config_key_found is True:
231 | break
232 | else:
233 | raise ValueError
234 |
235 | # raise error if the key is required and not found in any of the config files, envvar or cli args
236 | if config_key_found is False and required is True:
237 | raise ValueError
238 |
239 | # if key is not required but still wasn't found take it from the key default value or failing that from the
240 | # global default value
241 | if config_key_found is False:
242 | if default_value is not None:
243 | config_value = default_value
244 | else:
245 | config_value = self.global_default_value
246 |
247 | # if type estimation is True try to guess the type of the value
248 | if self.type_estimate is True:
249 | config_value = estimate_type(config_value, none_values=self.none_values)
250 |
251 | # if the type the config is in the end isn't in the list of allowed_types and allowed_types list is set raise
252 | # a TypeError
253 | if allowed_types is not None:
254 | if type(config_value) not in allowed_types:
255 | raise TypeError
256 |
257 | return config_value
258 |
259 | def read_multiple_configuration_variables(self, config_names: list, default_value: Any = None,
260 | required: bool = False, allowed_types: Optional[list] = None) -> dict:
261 | """reads multiple keys of the configuration and returns the first value of each it found based on the priority
262 | of each config file option given in the __init__ of the class, basically a simple loop of the
263 | read_configuration_variable function with all the configurable values being the same in all iterations
264 |
265 | Arguments:
266 | config_names -- a list of the configuration key names you want to get the value of
267 | default_value -- defaults to None, see config_type_priority in class init for it's use
268 | required -- defaults to False, if set to True will ignore default_value & global_default_value
269 | and will raise an ValueError if the configuration is not configured in any of the config
270 | files/envvars/cli args
271 | allowed_types -- Defaults to None, an optional list of types that are accepted for the variable
272 | to be, if set a check will be preformed and if the variables value given is not of any of
273 | the types in said list a TypeError will be raised
274 | Returns:
275 | config_value_dict -- a dict of the key/value pairs of all the configurations requested
276 | """
277 |
278 | config_value_dict = {}
279 | for config_name in config_names:
280 | config_value_dict[config_name] = self.read_configuration_variable(config_name, default_value, required,
281 | allowed_types)
282 | return config_value_dict
283 |
284 | def read_all_configuration_variables(self, default_value: Optional[dict] = None, required: Optional[list] = None,
285 | allowed_types: Optional[dict] = None) -> dict:
286 | """reads all configuration variables from all allowed sources and returns a dict that includes the combined
287 | result of all of them, if a configuration variable exists in two (or more) different sources the
288 | one with the higher priority will be the only one returned
289 |
290 | Arguments:
291 | default_value -- defaults to None, a dict of key/value pairs of a configuration variables & it's
292 | value should it not be defined in any of the valid sources
293 | required -- defaults to None, if given a list configuration variables it will raise a ValueError
294 | if any of the configuration variables is not configured in any of the config
295 | files/envvars/cli args
296 | allowed_types -- Defaults to None, an optional dict of types that are accepted for a variable to
297 | be, if set a check will be preformed and if the variables value given is not of any of the
298 | types in said list a TypeError will be raised
299 | Returns:
300 | config_value_dict -- a dict of the key/value pairs of all the configurations requested
301 | """
302 | # first we create an empty config_value_dict
303 | config_value_dict = {}
304 |
305 | # now we fill the config_value_dict with the data of all valid sources in reverse order (from least desired to
306 | # the most desired source), overwriting each data that is found multiple times with the more desired state
307 | data_sources = self.config_type_priority
308 | data_sources.reverse()
309 | for config_type in data_sources:
310 | if config_type == "cli_args":
311 | config_value_dict.update(read_all_cli_args_to_dict())
312 | elif config_type == "envvars" or config_type == "env_vars":
313 | if self.nest_envvars is True:
314 | config_value_dict.update(split_envvar_combained_dict(divider=self.envvar_divider,
315 | force_uppercase=self.force_envvars_uppercase))
316 | else:
317 | config_value_dict.update(read_all_envvars_to_dict(force_uppercase=self.force_envvars_uppercase))
318 | # will loop over all files of each type until all files of all types are searched, first time the key is
319 | # found will break outside of both loops
320 | elif config_type in self.valid_file_type_extension:
321 | for config_file in self.config_files_dict[config_type]:
322 | if self.config_file_type == "file":
323 | file_dict = self._parse_file_per_type(config_type, config_file)
324 | else:
325 | file_dict = self._parse_file_per_type(config_type, os.path.join(self.config_location,
326 | config_file))
327 | config_value_dict.update(file_dict)
328 | else:
329 | raise ValueError
330 |
331 | # now we need to add the default values from the provided "default_value" dict to any configuration variable in
332 | # said list that wasn't found in any of the valid sources
333 | if default_value is not None:
334 | for default_config_key, default_config_value in default_value.items():
335 | config_found, config_value = self._check_config_in_dict(default_config_key, config_value_dict)
336 | if config_found is False:
337 | config_value_dict[default_config_key] = default_config_value
338 |
339 | # and we run the type estimate (which is recursive) on the full dict if it's configured to be used
340 | if self.type_estimate is True:
341 | config_value_dict = estimate_type(config_value_dict, none_values=self.none_values)
342 |
343 | # now we check that all the required values exist and raise a ValueError otherwise
344 | if required is not None:
345 | for required_config in required:
346 | config_found, config_value = self._check_config_in_dict(required_config, config_value_dict)
347 | if config_found is False:
348 | raise ValueError
349 |
350 | # and we also check that the "allowed_types" of all keys in the dict are from the list of allowed types and
351 | # raise a TypeError
352 | if allowed_types is not None:
353 | for allowed_types_key, allowed_types_value in allowed_types.items():
354 | if type(config_value_dict[allowed_types_key]) not in allowed_types_value:
355 | raise TypeError
356 |
357 | # all that's left is returning the combined dict
358 | return config_value_dict
359 |
360 | @staticmethod
361 | def _check_config_in_dict(config_key: str, config_dict: dict) -> Tuple[bool, Any]:
362 | """internal function which checks if the key is in a given dict
363 |
364 | Arguments:
365 | config_key -- the configuration key name you want to check if it exists in the dict
366 | config_dict -- the dict you want to check if the is included in
367 | Returns:
368 | config_found -- True if the key is in the dict, false otherwise
369 | config_value -- the value of the configuration requested, returns None if the key is not part of the
370 | the dict
371 | """
372 |
373 | if config_key in config_dict:
374 | config_value = config_dict[config_key]
375 | config_found = True
376 | else:
377 | config_value = None
378 | config_found = False
379 | return config_found, config_value
380 |
381 | def _parse_file_per_type(self, config_file_type: str, config_file_location: str) -> dict:
382 | """internal function which parses a file to a dict when given the file format type and it's location
383 |
384 | Arguments:
385 | config_file_type -- the type of the config file
386 | config_file_location -- the location of the config file
387 | Returns:
388 | file_dict -- a parsed dict of the config file data
389 | """
390 |
391 | if config_file_type in self.suffix_file_type_mapping["json"]:
392 | file_dict = parse_json_file(config_file_location)
393 | elif config_file_type in self.suffix_file_type_mapping["yaml"]:
394 | file_dict = parse_yaml_file(config_file_location)
395 | elif config_file_type in self.suffix_file_type_mapping["toml"]:
396 | file_dict = parse_toml_file(config_file_location)
397 | elif config_file_type in self.suffix_file_type_mapping["ini"]:
398 | file_dict = parse_ini_file(config_file_location)
399 | elif config_file_type in self.suffix_file_type_mapping["hcl"]:
400 | file_dict = parse_hcl_file(config_file_location)
401 | elif config_file_type in self.suffix_file_type_mapping["xml"]:
402 | file_dict = parse_xml_file(config_file_location)
403 | elif config_file_type in self.suffix_file_type_mapping["env"]:
404 | file_dict = parse_env_file(config_file_location)
405 | else:
406 | raise ValueError
407 | return file_dict
408 |
--------------------------------------------------------------------------------
/test/test_prase_it.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase, mock
2 | from parse_it import ParseIt
3 | from parse_it.command_line_args.command_line_args import *
4 | from parse_it.envvars.envvars import *
5 | from parse_it.file.toml import *
6 | from parse_it.file.hcl import *
7 | from parse_it.file.yaml import *
8 | from parse_it.file.json import *
9 | from parse_it.file.env import *
10 | from parse_it.file.ini import *
11 | from parse_it.file.xml import *
12 | from parse_it.file.file_reader import *
13 | from parse_it.type_estimate.type_estimate import *
14 | import os
15 |
16 |
17 | VALID_FILE_TYPE_EXTENSIONS = [
18 | "json",
19 | "yaml",
20 | "yml",
21 | "toml",
22 | "tml",
23 | "conf",
24 | "cfg",
25 | "ini",
26 | "xml"
27 | ]
28 |
29 | test_files_location = os.getenv("TEST_FILES_LOCATION", "test/test_files")
30 |
31 |
32 | class BaseTests(TestCase):
33 |
34 | maxDiff = None
35 |
36 | def test_command_line_args_empty(self):
37 | reply = read_command_line_arg("empty_variable")
38 | self.assertIsNone(reply)
39 |
40 | def test_command_line_arg_defined_false(self):
41 | reply = command_line_arg_defined("empty_variable")
42 | self.assertFalse(reply)
43 |
44 | def test_envvars_empty(self):
45 | reply = read_envvar("empty_variable")
46 | self.assertIsNone(reply)
47 |
48 | def test_envvars_string(self):
49 | os.environ["TEST_ENVVAR"] = "TEST_ENVVAR_VALUE"
50 | reply = read_envvar("TEST_ENVVAR")
51 | self.assertEqual(reply, "TEST_ENVVAR_VALUE")
52 |
53 | def test_envvars_string_forced_uppercase(self):
54 | os.environ["TEST_ENVVAR_UPPERCASE"] = "TEST_ENVVAR_UPPERCASE_VALUE"
55 | reply = read_envvar("test_envvar_uppercase", force_uppercase=True)
56 | self.assertEqual(reply, "TEST_ENVVAR_UPPERCASE_VALUE")
57 |
58 | def test_envvars_string_forced_lowercase(self):
59 | os.environ["test_envvvar_lowercase"] = "test_envvvar_lowercase_value"
60 | reply = read_envvar("test_envvvar_lowercase", force_uppercase=False)
61 | self.assertEqual(reply, "test_envvvar_lowercase_value")
62 |
63 | def test_file_reader_read_file(self):
64 | reply = read_file(test_files_location + "/test_read_file")
65 | self.assertEqual(reply, "it_reads!")
66 |
67 | def test_file_reader_read_file_does_not_exist(self):
68 | reply = read_file(test_files_location + "/totally_bogus_file")
69 | self.assertIsNone(reply)
70 |
71 | def test_file_reader_file_or_folder_read_folder(self):
72 | reply = file_or_folder(test_files_location)
73 | self.assertEqual(reply, "folder")
74 |
75 | def test_file_reader_file_or_folder_read_file(self):
76 | reply = file_or_folder(test_files_location + "/test.json")
77 | self.assertEqual(reply, "file")
78 |
79 | def test_file_reader_file_or_folder_read_no_file(self):
80 | reply = file_or_folder("/non_existing_path")
81 | self.assertIsNone(reply)
82 |
83 | def test_file_reader_folder_exists(self):
84 | reply = folder_exists(test_files_location)
85 | self.assertTrue(reply)
86 |
87 | def test_file_reader_folder_does_not_exists(self):
88 | reply = folder_exists("totally_bogus_directory")
89 | self.assertFalse(reply)
90 |
91 | def test_file_reader_strip_trailing_slash(self):
92 | reply = strip_trailing_slash(test_files_location + "/")
93 | self.assertEqual(reply, test_files_location)
94 |
95 | def test_file_reader_strip_trailing_slash_root_folder(self):
96 | reply = strip_trailing_slash("/")
97 | self.assertEqual(reply, "/")
98 |
99 | def test_file_reader_strip_trailing_slash_no_strip_needed(self):
100 | reply = strip_trailing_slash(test_files_location)
101 | self.assertEqual(reply, test_files_location)
102 |
103 | def test_file_reader_file_types_in_folder_folder_does_not_exist_raise_warn(self):
104 | with self.assertWarns(Warning):
105 | file_types_in_folder("totally_bogus_folder_location", VALID_FILE_TYPE_EXTENSIONS)
106 |
107 | def test_read_envvar_folder_does_not_exist_raise_warn(self):
108 | with self.assertWarns(Warning):
109 | os.environ["TEST_CONFIG_FOLDER_NON_EXISTING_ENVVAR"] = "TEST_CONFIG_FOLDER_NON_EXISTING_ENVVAR"
110 | parser = ParseIt(config_location="totally_bogus_folder_location", config_type_priority=[
111 | "yaml",
112 | "json",
113 | "env_vars"
114 | ])
115 | reply = parser.read_configuration_variable("test_config_folder_non_existing_envvar")
116 | self.assertEqual(reply, "TEST_CONFIG_FOLDER_NON_EXISTING_ENVVAR")
117 |
118 | def test_read_envvar_single_file_config(self):
119 | os.environ["FILE_TYPE"] = "envvar"
120 | parser = ParseIt(config_location=test_files_location + "/test.hcl")
121 | reply = parser.read_configuration_variable("file_type")
122 | self.assertEqual(reply, "envvar")
123 | del os.environ["FILE_TYPE"]
124 |
125 | def test_read_cli_args_folder_does_not_exist_raise_warn(self):
126 | with self.assertWarns(Warning):
127 | ParseIt(config_location="totally_bogus_folder_location", config_type_priority=[
128 | "yaml",
129 | "json",
130 | "cli_args"
131 | ])
132 | test_args = ["parse_it_mock_script.py", "--test_cli_key_no_folder", "test_value"]
133 | with mock.patch('sys.argv', test_args):
134 | parser = ParseIt()
135 | reply = parser.read_configuration_variable("test_cli_key_no_folder")
136 | self.assertEqual(reply, "test_value")
137 |
138 | def test_file_reader_file_types_in_folder(self):
139 | reply = file_types_in_folder(test_files_location, VALID_FILE_TYPE_EXTENSIONS)
140 | expected_reply = {
141 | 'json': [
142 | 'test.json',
143 | 'test_none_values.json',
144 | 'test_subfolder_1/test_sub_subfolder_2/test_subfolder_2.json',
145 | 'test_subfolder_1/test_sub_subfolder_3/test_subfolder_3.json',
146 | 'test_subfolder_1/test_sub_subfolder_3/test_subfolder_4.json',
147 | 'test_subfolder_1/test_subfolder_1.json'
148 | ],
149 | 'yaml': [
150 | 'test.yaml'
151 | ],
152 | 'yml': [],
153 | 'toml': [
154 | 'test.toml'
155 | ],
156 | 'tml': [],
157 | 'conf': [],
158 | 'cfg': [],
159 | 'ini': [
160 | 'test.ini'
161 | ],
162 | 'xml': [
163 | 'test.xml'
164 | ]
165 | }
166 | self.assertDictEqual(reply, expected_reply)
167 |
168 | def test_ini(self):
169 | reply = parse_ini_file(test_files_location + "/test.ini")
170 | expected_reply = {
171 | 'DEFAULT': {
172 | 'file_type': "ini",
173 | 'test_string': 'testing',
174 | 'test_bool_true': 'true',
175 | 'test_bool_false': 'false',
176 | 'test_int': '123.0',
177 | 'test_float': '123.123',
178 | 'test_list': '["test1", "test2", "test3"]'
179 | },
180 | 'test_ini': {
181 | 'test_ini_key': 'test_ini_value'
182 | }
183 | }
184 | self.assertDictEqual(reply, expected_reply)
185 |
186 | def test_json(self):
187 | reply = parse_json_file(test_files_location + "/test.json")
188 | expected_reply = {
189 | 'file_type': "json",
190 | 'test_string': 'testing',
191 | 'test_bool_true': True,
192 | 'test_bool_false': False,
193 | 'test_int': 123,
194 | 'test_float': 123.123,
195 | 'test_list': [
196 | 'test1',
197 | 'test2',
198 | 'test3'
199 | ],
200 | 'test_json': {
201 | 'test_json_key': 'test_json_value'
202 | }
203 | }
204 | self.assertDictEqual(reply, expected_reply)
205 |
206 | def test_env_file(self):
207 | reply = parse_env_file(test_files_location + "/test.env")
208 | expected_reply = {
209 | 'file_type': 'env',
210 | 'test_string': 'testing',
211 | 'test_bool_true': 'true',
212 | 'test_bool_false': 'false',
213 | 'test_int': '123',
214 | 'test_float': '123.123',
215 | 'test_list': '["test1","test2","test3"]',
216 | 'test_env': '{"test_env_key": "test_env_value"}'}
217 | self.assertDictEqual(reply, expected_reply)
218 |
219 | def test_hcl(self):
220 | reply = parse_hcl_file(test_files_location + "/test.hcl")
221 | expected_reply = {
222 | 'file_type': "hcl",
223 | 'test_string': 'testing',
224 | 'test_bool_true': True,
225 | 'test_bool_false': False,
226 | 'test_int': 123,
227 | 'test_float': 123.123,
228 | 'test_dict': {
229 | "hcl_dict_key": "hcl_dict_value"
230 | },
231 | 'test_list': [
232 | 'test1',
233 | 'test2',
234 | 'test3'
235 | ],
236 | 'test_hcl': {
237 | 'test_hcl_name': {
238 | 'test_hcl_key': 'test_hcl_value'
239 | }
240 | }
241 | }
242 | self.assertDictEqual(reply, expected_reply)
243 |
244 | def test_xml(self):
245 | reply = parse_xml_file(test_files_location + "/test.xml")
246 | expected_reply = {
247 | "xml_root": {
248 | 'file_type': 'xml',
249 | 'test_bool_false': 'false',
250 | 'test_bool_true': 'true',
251 | 'test_float': '123.123',
252 | 'test_int': '123',
253 | 'test_xml': {
254 | 'test_xml_key': 'test_xml_value'
255 | },
256 | 'test_list': {
257 | 'element': [
258 | 'test1',
259 | 'test2',
260 | 'test3'
261 | ]
262 | },
263 | 'test_string': 'testing'
264 | }
265 | }
266 | self.assertDictEqual(reply, expected_reply)
267 |
268 | def test_toml(self):
269 | reply = parse_toml_file(test_files_location + "/test.toml")
270 | expected_reply = {
271 | 'file_type': "toml",
272 | 'test_string': 'testing',
273 | 'test_bool_true': True,
274 | 'test_bool_false': False,
275 | 'test_int': 123,
276 | 'test_float': 123.123,
277 | 'test_list': [
278 | 'test1',
279 | 'test2',
280 | 'test3'
281 | ],
282 | 'test_toml': {
283 | 'test_toml_key': 'test_toml_value'
284 | }
285 | }
286 | self.assertDictEqual(reply, expected_reply)
287 |
288 | def test_yaml(self):
289 | reply = parse_yaml_file(test_files_location + "/test.yaml")
290 | expected_reply = {
291 | 'file_type': "yaml",
292 | 'test_string': 'testing',
293 | 'test_bool_true': True,
294 | 'test_bool_false': False,
295 | 'test_int': 123,
296 | 'test_float': 123.123,
297 | 'test_list': [
298 | 'test1',
299 | 'test2',
300 | 'test3'
301 | ],
302 | 'test_yaml': {
303 | 'test_yaml_key': 'test_yaml_value'
304 | }
305 | }
306 | self.assertDictEqual(reply, expected_reply)
307 |
308 | def test_type_estimate_string(self):
309 | reply = estimate_type("this_is_a_string")
310 | self.assertEqual(reply, "this_is_a_string")
311 |
312 | def test_type_estimate_string_complex(self):
313 | reply = estimate_type("kafka:8082")
314 | self.assertEqual(reply, "kafka:8082")
315 |
316 | def test_type_estimate_string_very_complex(self):
317 | reply = estimate_type("https://test//seg34\\^#%#^&@@GGH\nE#TGddvs.36230.54164:8082")
318 | self.assertEqual(reply, "https://test//seg34\\^#%#^&@@GGH\nE#TGddvs.36230.54164:8082")
319 |
320 | def test_type_estimate_false(self):
321 | reply_lowercase = estimate_type("false")
322 | reply_uppercase = estimate_type("FALSE")
323 | reply_mixed = estimate_type("False")
324 | self.assertEqual(reply_lowercase, False)
325 | self.assertEqual(reply_uppercase, False)
326 | self.assertEqual(reply_mixed, False)
327 |
328 | def test_type_estimate_true(self):
329 | reply_lowercase = estimate_type("true")
330 | reply_uppercase = estimate_type("TRUE")
331 | reply_mixed = estimate_type("True")
332 | self.assertEqual(reply_lowercase, True)
333 | self.assertEqual(reply_uppercase, True)
334 | self.assertEqual(reply_mixed, True)
335 |
336 | def test_type_estimate_int(self):
337 | reply = estimate_type("123")
338 | self.assertEqual(reply, 123)
339 |
340 | def test_type_estimate_float(self):
341 | reply = estimate_type("123.123")
342 | self.assertEqual(reply, 123.123)
343 |
344 | def test_type_estimate_list(self):
345 | reply = estimate_type("['test1', 123, True]")
346 | self.assertEqual(reply, ['test1', 123, True])
347 | reply = estimate_type('["test1", "test2", "test3"]')
348 | self.assertListEqual(reply, ["test1", "test2", "test3"])
349 | reply = estimate_type('["test1", {"test_key": "test_value"}, "test3", None]')
350 | self.assertListEqual(reply, ["test1", {"test_key": "test_value"}, "test3", None])
351 |
352 | def test_type_estimate_dict(self):
353 | reply = estimate_type("{'test_key': ['test1', 123, {'key': 'value'}]}")
354 | self.assertEqual(reply, {'test_key': ['test1', 123, {'key': 'value'}]})
355 |
356 | def test_type_estimate_none(self):
357 | reply_empty = estimate_type("")
358 | reply_lowercase = estimate_type("none")
359 | reply_null = estimate_type("null")
360 | reply_uppercase = estimate_type("NONE")
361 | reply_mixed = estimate_type("None")
362 | self.assertEqual(reply_empty, None)
363 | self.assertEqual(reply_lowercase, None)
364 | self.assertEqual(reply_null, None)
365 | self.assertEqual(reply_uppercase, None)
366 | self.assertEqual(reply_mixed, None)
367 |
368 | def test_type_estimate_none_custom(self):
369 | allowed_none_values = {"null", "none"}
370 | reply_empty = estimate_type("", allowed_none_values)
371 | reply_lowercase = estimate_type("none", allowed_none_values)
372 | reply_null = estimate_type("null", allowed_none_values)
373 | reply_uppercase = estimate_type("NONE", allowed_none_values)
374 | reply_mixed = estimate_type("None", allowed_none_values)
375 | self.assertNotEqual(reply_empty, None)
376 | self.assertEqual(reply_lowercase, None)
377 | self.assertEqual(reply_null, None)
378 | self.assertEqual(reply_uppercase, None)
379 | self.assertEqual(reply_mixed, None)
380 |
381 | def test_parser_init_no_recurse(self):
382 | expected_config_files_dict = {
383 | 'env': [
384 | "test.env"
385 | ],
386 | 'json': [
387 | 'test.json',
388 | 'test_none_values.json'
389 | ],
390 | 'yaml': [
391 | 'test.yaml'
392 | ],
393 | 'yml': [],
394 | 'toml': ['test.toml'],
395 | 'tml': [],
396 | 'hcl': ['test.hcl'],
397 | 'tf': [],
398 | 'conf': [],
399 | 'cfg': [],
400 | 'ini': [
401 | 'test.ini'
402 | ],
403 | 'xml': [
404 | 'test.xml'
405 | ]
406 | }
407 | expected_config_type_priority = ['cli_args', 'env_vars', "env", 'json', 'yaml', 'yml', 'toml', 'tml', 'hcl',
408 | 'tf', 'conf', 'cfg', 'ini', 'xml']
409 | parser = ParseIt(config_type_priority=None, global_default_value=None, type_estimate=True,
410 | force_envvars_uppercase=True, config_location=test_files_location, envvar_prefix=None)
411 | self.assertDictEqual(parser.config_files_dict, expected_config_files_dict)
412 | self.assertEqual(parser.config_location, test_files_location)
413 | self.assertListEqual(parser.config_type_priority, expected_config_type_priority)
414 | self.assertEqual(parser.envvar_prefix, '')
415 | self.assertTrue(parser.force_envvars_uppercase)
416 | self.assertIsNone(parser.global_default_value)
417 | self.assertTrue(parser.type_estimate)
418 |
419 | def test_parser_init_recurse(self):
420 | expected_config_files_dict = {
421 | 'env': [
422 | 'test.env'
423 | ],
424 | 'json': [
425 | 'test.json',
426 | 'test_none_values.json',
427 | 'test_subfolder_1/test_sub_subfolder_2/test_subfolder_2.json',
428 | 'test_subfolder_1/test_sub_subfolder_3/test_subfolder_3.json',
429 | 'test_subfolder_1/test_sub_subfolder_3/test_subfolder_4.json',
430 | 'test_subfolder_1/test_subfolder_1.json'
431 | ],
432 | 'yaml': [
433 | 'test.yaml'
434 | ],
435 | 'yml': [],
436 | 'toml': ['test.toml'],
437 | 'tml': [],
438 | 'hcl': ['test.hcl'],
439 | 'tf': [],
440 | 'conf': [],
441 | 'cfg': [],
442 | 'ini': [
443 | 'test.ini'
444 | ],
445 | 'xml': [
446 | 'test.xml'
447 | ]
448 | }
449 | expected_config_type_priority = ['cli_args', 'env_vars', 'env', 'json', 'yaml', 'yml', 'toml', 'tml', 'hcl',
450 | 'tf', 'conf', 'cfg', 'ini', 'xml']
451 | parser = ParseIt(config_type_priority=None, global_default_value=None, type_estimate=True, recurse=True,
452 | force_envvars_uppercase=True, config_location=test_files_location, envvar_prefix=None)
453 | self.assertDictEqual(parser.config_files_dict, expected_config_files_dict)
454 | self.assertEqual(parser.config_location, test_files_location)
455 | self.assertListEqual(parser.config_type_priority, expected_config_type_priority)
456 | self.assertEqual(parser.envvar_prefix, '')
457 | self.assertTrue(parser.force_envvars_uppercase)
458 | self.assertIsNone(parser.global_default_value)
459 | self.assertTrue(parser.type_estimate)
460 |
461 | def test_parser_init_single_file(self):
462 | expected_config_files_dict = {
463 | 'env': [],
464 | 'cfg': [],
465 | 'conf': [],
466 | 'hcl': [],
467 | 'ini': [],
468 | 'json': [
469 | 'test/test_files/test.json'
470 | ],
471 | 'tf': [],
472 | 'tml': [],
473 | 'toml': [],
474 | 'xml': [],
475 | 'yaml': [],
476 | 'yml': []
477 | }
478 | expected_config_type_priority = ['cli_args', 'env_vars', 'env', 'json', 'yaml', 'yml', 'toml', 'tml', 'hcl',
479 | 'tf', 'conf', 'cfg', 'ini', 'xml']
480 | parser = ParseIt(config_type_priority=None, global_default_value=None, type_estimate=True,
481 | force_envvars_uppercase=True, config_location=test_files_location + "/test.json",
482 | envvar_prefix=None)
483 | self.assertDictEqual(parser.config_files_dict, expected_config_files_dict)
484 | self.assertEqual(parser.config_location, test_files_location + "/test.json")
485 | self.assertListEqual(parser.config_type_priority, expected_config_type_priority)
486 | self.assertEqual(parser.envvar_prefix, '')
487 | self.assertTrue(parser.force_envvars_uppercase)
488 | self.assertIsNone(parser.global_default_value)
489 | self.assertTrue(parser.type_estimate)
490 |
491 | def test_parser_init_envvar_divider_none(self):
492 | parser = ParseIt(config_location=test_files_location + "/test.json", envvar_divider=None)
493 | self.assertFalse(parser.nest_envvars)
494 |
495 | def test_parser_init_envvar_divider_set(self):
496 | test_envvar_divider = "_"
497 | parser = ParseIt(config_location=test_files_location + "/test.json", envvar_divider=test_envvar_divider)
498 | self.assertTrue(parser.nest_envvars)
499 | self.assertEqual(test_envvar_divider, parser.envvar_divider)
500 |
501 | def test_parser_custom_suffix_mapping_set_config_type_priority_not_set_raise_warning(self):
502 | with self.assertWarns(Warning):
503 | ParseIt(config_location=test_files_location, custom_suffix_mapping={"yaml": ["custom"]})
504 |
505 | def test_parser_custom_suffix_mapping_set(self):
506 | parser = ParseIt(config_location=test_files_location, custom_suffix_mapping={"yaml": ["custom"]},
507 | config_type_priority=["custom"] + VALID_FILE_TYPE_EXTENSIONS)
508 | expected_config_type_priority = ["custom"] + VALID_FILE_TYPE_EXTENSIONS
509 | expected_valid_type_extension = ['env', 'json', 'yaml', 'yml', 'toml', 'tml', 'hcl', 'tf', 'conf', 'cfg', 'ini',
510 | 'xml', 'custom']
511 | expected_suffix_file_type_mapping = {
512 | 'env': [
513 | 'env'
514 | ],
515 | 'json': [
516 | 'json'
517 | ],
518 | 'yaml': [
519 | 'yaml',
520 | 'yml',
521 | 'custom'
522 | ],
523 | 'toml': [
524 | 'toml',
525 | 'tml'
526 | ],
527 | 'hcl': [
528 | 'hcl',
529 | 'tf'
530 | ],
531 | 'ini': [
532 | 'conf',
533 | 'cfg',
534 | 'ini'
535 | ],
536 | 'xml': [
537 | 'xml'
538 | ]
539 | }
540 | reply = parser.read_configuration_variable("test_string")
541 | self.assertEqual("testing_custom", reply)
542 | reply = parser.read_configuration_variable("test_json")
543 | self.assertEqual({'test_json_key': 'test_json_value'}, reply)
544 | self.assertListEqual(expected_config_type_priority, parser.config_type_priority)
545 | self.assertListEqual(expected_valid_type_extension, parser.valid_file_type_extension)
546 | self.assertDictEqual(expected_suffix_file_type_mapping, parser.suffix_file_type_mapping)
547 |
548 | def test_parser_read_configuration_variable(self):
549 | parser = ParseIt(config_location=test_files_location)
550 | reply_json = parser.read_configuration_variable("file_type")
551 | self.assertEqual(reply_json, "env")
552 | reply_yaml = parser.read_configuration_variable("test_yaml")
553 | self.assertDictEqual(reply_yaml, {'test_yaml_key': 'test_yaml_value'})
554 |
555 | def test_parser_read_configuration_subfolder(self):
556 | parser = ParseIt(config_location=test_files_location, recurse=True)
557 | reply = parser.read_configuration_variable("test_json_subfolder")
558 | self.assertTrue(reply)
559 |
560 | def test_parser_read_configuration_recurse_false(self):
561 | parser = ParseIt(config_location=test_files_location, recurse=False)
562 | reply = parser.read_configuration_variable("test_json_subfolder")
563 | expected_config_files_dict = {
564 | 'env': [
565 | 'test.env'
566 | ],
567 | 'json': [
568 | 'test.json',
569 | 'test_none_values.json'
570 | ],
571 | 'yaml': [
572 | 'test.yaml'
573 | ],
574 | 'yml': [],
575 | 'toml': [
576 | 'test.toml'
577 | ],
578 | 'tml': [],
579 | 'conf': [],
580 | 'hcl': ['test.hcl'],
581 | 'tf': [],
582 | 'cfg': [],
583 | 'ini': [
584 | 'test.ini'
585 | ],
586 | 'xml': [
587 | 'test.xml'
588 | ]
589 | }
590 | self.assertDictEqual(parser.config_files_dict, expected_config_files_dict)
591 | self.assertIsNone(reply)
592 |
593 | def test_parser_read_configuration_single_file(self):
594 | parser = ParseIt(config_location=test_files_location + "/test.hcl")
595 | reply = parser.read_configuration_variable("file_type")
596 | expected_reply = "hcl"
597 | self.assertEqual(reply, expected_reply)
598 |
599 | def test_parser_read_configuration_variable_default_value(self):
600 | parser = ParseIt(config_location=test_files_location)
601 | reply = parser.read_configuration_variable("file_type123", default_value="test123")
602 | self.assertEqual(reply, "test123")
603 |
604 | def test_parser_read_configuration_variable_required_true_value_not_given(self):
605 | parser = ParseIt(config_location=test_files_location)
606 | with self.assertRaises(ValueError):
607 | parser.read_configuration_variable("file_type123", required=True)
608 |
609 | def test_parser_config_type_priority_wrong_type_given(self):
610 | with self.assertRaises(ValueError):
611 | parser = ParseIt(config_type_priority=['non_existing_type', 'json'])
612 | parser.read_configuration_variable("file_type123", required=True)
613 |
614 | def test_parser_read_configuration_variable_required_true_value_given_file(self):
615 | parser = ParseIt(config_location=test_files_location)
616 | reply_json = parser.read_configuration_variable("file_type", required=True)
617 | self.assertEqual(reply_json, "env")
618 |
619 | def test_parser_read_configuration_variable_required_true_value_given_envvar(self):
620 | os.environ["TEST_ENVVAR_ESTIMATE_TRUE_INT"] = "123"
621 | parser = ParseIt(config_location=test_files_location)
622 | reply_json = parser.read_configuration_variable("TEST_ENVVAR_ESTIMATE_TRUE_INT", required=True)
623 | self.assertEqual(reply_json, 123)
624 |
625 | def test_parser_read_configuration_variable_global_default_value(self):
626 | parser = ParseIt(global_default_value="my_last_resort", config_location=test_files_location)
627 | reply = parser.read_configuration_variable("this_does_not_exist")
628 | self.assertEqual(reply, "my_last_resort")
629 |
630 | def test_parser_read_configuration_variable_config_type_priority(self):
631 | os.environ["TEST_ENVVAR_ESTIMATE_TRUE_INT"] = "123"
632 | parser = ParseIt(config_type_priority=["yaml", "toml", "ini", "json", "envvars", "env"],
633 | config_location=test_files_location)
634 | reply = parser.read_configuration_variable("file_type")
635 | self.assertEqual(reply, "yaml")
636 | reply = parser.read_configuration_variable("test_toml")
637 | self.assertDictEqual(reply, {'test_toml_key': 'test_toml_value'})
638 | reply = parser.read_configuration_variable("test_ini")
639 | self.assertDictEqual(reply, {'test_ini_key': 'test_ini_value'})
640 | reply = parser.read_configuration_variable("test_json")
641 | self.assertDictEqual(reply, {'test_json_key': 'test_json_value'})
642 | reply = parser.read_configuration_variable("TEST_ENVVAR_ESTIMATE_TRUE_INT")
643 | self.assertEqual(reply, 123)
644 | reply = parser.read_configuration_variable("test_list")
645 | self.assertEqual(reply, ['test1', 'test2', 'test3'])
646 |
647 | def test_parser_read_configuration_variable_type_estimate_false(self):
648 | parser = ParseIt(type_estimate=False, config_location=test_files_location)
649 | os.environ["TEST_ENVVAR_ESTIMATE_FALSE_INT"] = "123"
650 | os.environ["TEST_ENVVAR_ESTIMATE_FALSE_STRING"] = "test"
651 | os.environ["TEST_ENVVAR_ESTIMATE_FALSE_BOOL_TRUE"] = "true"
652 | os.environ["TEST_ENVVAR_ESTIMATE_FALSE_BOOL_FALSE"] = "false"
653 | os.environ["TEST_ENVVAR_ESTIMATE_FALSE_LIST"] = "['test', False, 3]"
654 | os.environ["TEST_ENVVAR_ESTIMATE_FALSE_DICT"] = "{'string': 'string', 'int': 1}"
655 | os.environ["TEST_ENVVAR_ESTIMATE_FALSE_RECURSIVE_LIST"] = "['test', False, 3, ['test', True, 35, {'test': 12}]]"
656 | os.environ["TEST_ENVVAR_ESTIMATE_FALSE_RECURSIVE_DICT"] = "{'string': 'string', 'int': 1, 'dict': " \
657 | "{'test': [1, 2]}}"
658 | reply = parser.read_configuration_variable("TEST_ENVVAR_ESTIMATE_FALSE_INT")
659 | self.assertEqual(reply, "123")
660 | self.assertNotEqual(reply, 123)
661 | reply = parser.read_configuration_variable("TEST_ENVVAR_ESTIMATE_FALSE_STRING")
662 | self.assertEqual(reply, "test")
663 | reply = parser.read_configuration_variable("TEST_ENVVAR_ESTIMATE_FALSE_BOOL_TRUE")
664 | self.assertEqual(reply, "true")
665 | self.assertNotEqual(reply, True)
666 | reply = parser.read_configuration_variable("TEST_ENVVAR_ESTIMATE_FALSE_BOOL_FALSE")
667 | self.assertEqual(reply, "false")
668 | self.assertNotEqual(reply, False)
669 | reply = parser.read_configuration_variable("TEST_ENVVAR_ESTIMATE_FALSE_LIST")
670 | self.assertEqual(reply, "['test', False, 3]")
671 | self.assertNotEqual(reply, ['test', False, 3])
672 | reply = parser.read_configuration_variable("TEST_ENVVAR_ESTIMATE_FALSE_DICT")
673 | self.assertEqual(reply, "{'string': 'string', 'int': 1}")
674 | self.assertNotEqual(reply, {'string': 'string', 'int': 1})
675 | reply = parser.read_configuration_variable("TEST_ENVVAR_ESTIMATE_FALSE_RECURSIVE_LIST")
676 | self.assertEqual(reply, "['test', False, 3, ['test', True, 35, {'test': 12}]]")
677 | self.assertNotEqual(reply, ['test', False, 3, ['test', True, 35, {'test': 12}]])
678 | reply = parser.read_configuration_variable("TEST_ENVVAR_ESTIMATE_FALSE_RECURSIVE_DICT")
679 | self.assertEqual(reply, "{'string': 'string', 'int': 1, 'dict': {'test': [1, 2]}}")
680 | self.assertNotEqual(reply, {'string': 'string', 'int': 1, 'dict': {'test': [1, 2]}})
681 |
682 | def test_parser_read_configuration_variable_type_estimate_true(self):
683 | parser = ParseIt(type_estimate=True, config_location=test_files_location)
684 | os.environ["TEST_ENVVAR_ESTIMATE_TRUE_INT"] = "123"
685 | os.environ["TEST_ENVVAR_ESTIMATE_TRUE_STRING"] = "test"
686 | os.environ["TEST_ENVVAR_ESTIMATE_TRUE_BOOL_TRUE"] = "true"
687 | os.environ["TEST_ENVVAR_ESTIMATE_TRUE_BOOL_FALSE"] = "false"
688 | os.environ["TEST_ENVVAR_ESTIMATE_TRUE_LIST"] = "['test', False, 3]"
689 | os.environ["TEST_ENVVAR_ESTIMATE_TRUE_DICT"] = "{'string': 'string', 'int': 1}"
690 | os.environ["TEST_ENVVAR_ESTIMATE_TRUE_RECURSIVE_LIST"] = "['test', False, 3, ['test', True, 35, {'test': 12}]]"
691 | os.environ["TEST_ENVVAR_ESTIMATE_TRUE_RECURSIVE_DICT"] = "{'string': 'string', 'int': 1, 'dict': " \
692 | "{'test': [1, 2]}}"
693 | reply = parser.read_configuration_variable("TEST_ENVVAR_ESTIMATE_TRUE_INT")
694 | self.assertNotEqual(reply, "123")
695 | self.assertEqual(reply, 123)
696 | reply = parser.read_configuration_variable("TEST_ENVVAR_ESTIMATE_TRUE_STRING")
697 | self.assertEqual(reply, "test")
698 | reply = parser.read_configuration_variable("TEST_ENVVAR_ESTIMATE_TRUE_BOOL_TRUE")
699 | self.assertNotEqual(reply, "true")
700 | self.assertEqual(reply, True)
701 | reply = parser.read_configuration_variable("TEST_ENVVAR_ESTIMATE_TRUE_BOOL_FALSE")
702 | self.assertNotEqual(reply, "false")
703 | self.assertEqual(reply, False)
704 | reply = parser.read_configuration_variable("TEST_ENVVAR_ESTIMATE_TRUE_LIST")
705 | self.assertNotEqual(reply, "['test', False, 3]")
706 | self.assertListEqual(reply, ['test', False, 3])
707 | reply = parser.read_configuration_variable("TEST_ENVVAR_ESTIMATE_TRUE_DICT")
708 | self.assertNotEqual(reply, "{'string': 'string', 'int': 1}")
709 | self.assertDictEqual(reply, {'string': 'string', 'int': 1})
710 | reply = parser.read_configuration_variable("TEST_ENVVAR_ESTIMATE_TRUE_RECURSIVE_LIST")
711 | self.assertNotEqual(reply, "['test', False, 3, ['test', True, 35, {'test': 12}]]")
712 | self.assertListEqual(reply, ['test', False, 3, ['test', True, 35, {'test': 12}]])
713 | reply = parser.read_configuration_variable("TEST_ENVVAR_ESTIMATE_TRUE_RECURSIVE_DICT")
714 | self.assertNotEqual(reply, "{'string': 'string', 'int': 1, 'dict': {'test': [1, 2]}}")
715 | self.assertDictEqual(reply, {'string': 'string', 'int': 1, 'dict': {'test': [1, 2]}})
716 |
717 | def test_parser_read_configuration_variable_force_envvars_uppercase_true(self):
718 | parser = ParseIt(force_envvars_uppercase=True)
719 | os.environ["TEST_ENVVAR_ESTIMATE_TRUE_INT"] = "123"
720 | os.environ["test_envvar_estimate_true_int"] = "456"
721 | reply = parser.read_configuration_variable("test_envvar_estimate_true_int")
722 | self.assertEqual(reply, 123)
723 | self.assertNotEqual(reply, 456)
724 |
725 | def test_parser_read_configuration_variable_force_envvars_uppercase_false(self):
726 | parser = ParseIt(force_envvars_uppercase=False, config_location=test_files_location)
727 | os.environ["TEST_ENVVAR_ESTIMATE_TRUE_INT"] = "123"
728 | os.environ["test_envvar_estimate_true_int"] = "456"
729 | reply = parser.read_configuration_variable("test_envvar_estimate_true_int")
730 | self.assertNotEqual(reply, 123)
731 | self.assertEqual(reply, 456)
732 |
733 | def test_parser_read_configuration_variable_config_location(self):
734 | parser = ParseIt(config_location=test_files_location)
735 | reply_json = parser.read_configuration_variable("file_type")
736 | self.assertEqual(reply_json, "env")
737 | reply_json = parser.read_configuration_variable("test_float")
738 | self.assertEqual(reply_json, 123.123)
739 | reply_yaml = parser.read_configuration_variable("test_yaml")
740 | self.assertDictEqual(reply_yaml, {'test_yaml_key': 'test_yaml_value'})
741 | reply_xml = parser.read_configuration_variable("xml_root")
742 | expected_reply_xml = {
743 | 'file_type': 'xml',
744 | 'test_bool_false': False,
745 | 'test_bool_true': True,
746 | 'test_float': 123.123,
747 | 'test_int': 123,
748 | 'test_xml': {
749 | 'test_xml_key': 'test_xml_value'
750 | },
751 | 'test_list': {
752 | 'element': [
753 | 'test1',
754 | 'test2',
755 | 'test3'
756 | ]
757 | },
758 | 'test_string': 'testing'
759 | }
760 | self.assertDictEqual(reply_xml, expected_reply_xml)
761 | reply_hcl = parser.read_configuration_variable("test_hcl")
762 | expected_reply_hcl = {
763 | 'test_hcl_name': {
764 | 'test_hcl_key': 'test_hcl_value'
765 | }
766 | }
767 | self.assertDictEqual(reply_hcl, expected_reply_hcl)
768 |
769 | def test_parser_read_configuration_variable_envvar_prefix(self):
770 | parser = ParseIt(envvar_prefix="prefix_test_", config_location=test_files_location)
771 | os.environ["PREFIX_TEST_TEST_ENVVAR_ESTIMATE_TRUE_INT"] = "123"
772 | reply = parser.read_configuration_variable("test_envvar_estimate_true_int")
773 | self.assertEqual(reply, 123)
774 |
775 | def test_parser_read_configuration_variable_envvar_nested(self):
776 | parser = ParseIt(config_location=test_files_location, envvar_divider="_")
777 | os.environ["TEST_ENVVAR_NESTED"] = "123"
778 | reply = parser.read_configuration_variable("test_envvar_nested")
779 | self.assertDictEqual(reply, {"test": {"envvar": {"nested": 123}}})
780 |
781 | def test_envvar_defined_true(self):
782 | os.environ["TEST_ENV"] = "123"
783 | reply = envvar_defined("TEST_ENV")
784 | self.assertTrue(reply)
785 | del os.environ["TEST_ENV"]
786 |
787 | def test_envvar_defined_false(self):
788 | reply = envvar_defined("TEST_ENV")
789 | self.assertFalse(reply)
790 |
791 | def test_envvar_defined_true_upper_case(self):
792 | os.environ["TEST_ENV"] = "123"
793 | reply = envvar_defined("test_env", force_uppercase=True)
794 | self.assertTrue(reply)
795 | del os.environ["TEST_ENV"]
796 |
797 | def test_envvar_defined_false_upper_case(self):
798 | os.environ["TEST_ENV"] = "123"
799 | reply = envvar_defined("test_env", force_uppercase=False)
800 | self.assertFalse(reply)
801 | del os.environ["TEST_ENV"]
802 |
803 | def test_read_all_envvars_to_dict_force_uppercase_true(self):
804 | test_envvars = {"TEST_ENV": "123", "test_env_lowercase": "456"}
805 | with mock.patch.dict(os.environ, test_envvars):
806 | reply = read_all_envvars_to_dict(force_uppercase=True)
807 | self.assertEqual(type(reply), dict)
808 | self.assertEqual(reply["test_env"], "123")
809 | self.assertEqual(reply["test_env_lowercase"], "456")
810 |
811 | def test_read_all_envvars_to_dict_force_uppercase_false(self):
812 | test_envvars = {"TEST_ENV": "123", "test_env_lowercase": "456"}
813 | with mock.patch.dict(os.environ, test_envvars):
814 | reply = read_all_envvars_to_dict(force_uppercase=False)
815 | self.assertEqual(type(reply), dict)
816 | self.assertEqual(reply["TEST_ENV"], "123")
817 | self.assertEqual(reply["test_env_lowercase"], "456")
818 |
819 | def test_envvars_split_force_uppercase_false(self):
820 | expected_reply = {'dict': {'subdict': {'key': 'example_value'}}}
821 | test_key = "dict_subdict_key"
822 | test_value = "example_value"
823 | reply = split_envvar(test_key, test_value, divider="_")
824 | self.assertEqual(expected_reply, reply)
825 |
826 | def test_envvars_split_force_uppercase_true(self):
827 | expected_reply = {'dict': {'subdict': {'key': 'example_value'}}}
828 | test_key = "dict_subdict_key"
829 | test_value = "example_value"
830 | reply = split_envvar(test_key, test_value, divider="_")
831 | self.assertEqual(expected_reply, reply)
832 |
833 | def test_envvars_split_no_nesting_needed(self):
834 | expected_reply = {'dict': 'example_value'}
835 | test_key = "dict"
836 | test_value = "example_value"
837 | reply = split_envvar(test_key, test_value, divider="_")
838 | self.assertEqual(expected_reply, reply)
839 |
840 | def test_envvars_split_custom_divider(self):
841 | expected_reply = {'dict': {'subdict': {'key': 'example_value'}}}
842 | test_key = "dict.subdict.key"
843 | test_value = "example_value"
844 | reply = split_envvar(test_key, test_value, divider=".")
845 | self.assertEqual(expected_reply, reply)
846 |
847 | def test_envvars_split_envvar_combained_dict(self):
848 | test_envvars = {"TEST_SPLIT_UPPERCASE1": "test123", "test_split_lowercase1": "test456"}
849 | with mock.patch.dict(os.environ, test_envvars):
850 | reply = split_envvar_combained_dict()
851 | self.assertEqual(type(reply), dict)
852 | self.assertEqual(reply["test"]["split"]["uppercase1"], "test123")
853 | self.assertEqual(reply["test"]["split"]["lowercase1"], "test456")
854 |
855 | def test_envvars_split_envvar_combained_dict_custom_divider(self):
856 | test_envvars = {"TEST.SPLIT.UPPER_CASE2": "test123", "test.split.lower_case2": "test456"}
857 | with mock.patch.dict(os.environ, test_envvars):
858 | reply = split_envvar_combained_dict(divider=".")
859 | self.assertEqual(type(reply), dict)
860 | self.assertEqual(reply["test"]["split"]["upper_case2"], "test123")
861 | self.assertEqual(reply["test"]["split"]["lower_case2"], "test456")
862 |
863 | def test_envvars_split_envvar_combained_dict_force_uppercase_false(self):
864 | test_envvars = {
865 | "TEST_SPLIT_UPPERCASE3": "test123",
866 | "test_split_lowercase3": "test456",
867 | "test_split_lowercase4": "test789"
868 | }
869 | with mock.patch.dict(os.environ, test_envvars):
870 | reply = split_envvar_combained_dict(force_uppercase=False)
871 | self.assertEqual(type(reply), dict)
872 | self.assertEqual(reply["TEST"]["SPLIT"]["UPPERCASE3"], "test123")
873 | self.assertEqual(reply["test"]["split"]["lowercase3"], "test456")
874 | self.assertEqual(reply["test"]["split"]["lowercase4"], "test789")
875 |
876 | def test_parser_config_found_in_key(self):
877 | parser = ParseIt(config_location=test_files_location)
878 | config_found_reply, config_value_reply = parser._check_config_in_dict("test_key", {"test_key": "test_value"})
879 | self.assertTrue(config_found_reply)
880 | self.assertEqual(config_value_reply, "test_value")
881 |
882 | def test_parser_config_found_in_key_false(self):
883 | parser = ParseIt(config_location=test_files_location)
884 | config_found_reply, config_value_reply = parser._check_config_in_dict("wrong_key", {"test_key": "test_value"})
885 | self.assertFalse(config_found_reply)
886 | self.assertEqual(config_value_reply, None)
887 |
888 | def test_parser__parse_file_per_type_wrong_type(self):
889 | parser = ParseIt(config_location=test_files_location)
890 | with self.assertRaises(ValueError):
891 | parser._parse_file_per_type("non_existing_type", test_files_location + "/test.json")
892 |
893 | def test_parser__parse_file_per_type_json(self):
894 | parser = ParseIt(config_location=test_files_location)
895 | reply = parser._parse_file_per_type("json", test_files_location + "/test.json")
896 | expected_reply = {
897 | 'file_type': 'json',
898 | 'test_string': 'testing',
899 | 'test_bool_true': True,
900 | 'test_bool_false': False,
901 | 'test_int': 123,
902 | 'test_float': 123.123,
903 | 'test_list': [
904 | 'test1',
905 | 'test2',
906 | 'test3'
907 | ],
908 | 'test_json': {
909 | 'test_json_key': 'test_json_value'
910 | }
911 | }
912 | self.assertDictEqual(reply, expected_reply)
913 |
914 | def test_parser__parse_file_per_type_yaml(self):
915 | parser = ParseIt(config_location=test_files_location)
916 | reply = parser._parse_file_per_type("yaml", test_files_location + "/test.yaml")
917 | expected_reply = {
918 | 'file_type': 'yaml',
919 | 'test_string': 'testing',
920 | 'test_bool_true': True,
921 | 'test_bool_false': False,
922 | 'test_int': 123,
923 | 'test_float': 123.123,
924 | 'test_list': [
925 | 'test1',
926 | 'test2',
927 | 'test3'
928 | ],
929 | 'test_yaml': {
930 | 'test_yaml_key': 'test_yaml_value'
931 | }
932 | }
933 | self.assertDictEqual(reply, expected_reply)
934 | reply = parser._parse_file_per_type("yml", test_files_location + "/test.yaml")
935 | self.assertDictEqual(reply, expected_reply)
936 |
937 | def test_parser__parse_file_per_type_custom_yaml(self):
938 | parser = ParseIt(config_location=test_files_location, custom_suffix_mapping={"yaml": ["custom"]},
939 | config_type_priority=["custom"] + VALID_FILE_TYPE_EXTENSIONS)
940 | reply = parser._parse_file_per_type("custom", test_files_location + "/test.custom")
941 | expected_reply = {
942 | 'file_type': 'custom_yaml_suffix',
943 | 'test_string': 'testing_custom',
944 | 'test_bool_true': True,
945 | 'test_bool_false': False,
946 | 'test_int': 123,
947 | 'test_float': 123.123,
948 | 'test_list': [
949 | 'test1',
950 | 'test2',
951 | 'test3'
952 | ],
953 | 'test_yaml': {
954 | 'test_yaml_key': 'custom_test_yaml_value'
955 | }
956 | }
957 | self.assertDictEqual(reply, expected_reply)
958 |
959 | def test_parser__parse_file_per_type_toml(self):
960 | parser = ParseIt(config_location=test_files_location)
961 | reply = parser._parse_file_per_type("toml", test_files_location + "/test.toml")
962 | expected_reply = {
963 | 'file_type': 'toml',
964 | 'test_string': 'testing',
965 | 'test_bool_true': True,
966 | 'test_bool_false': False,
967 | 'test_int': 123,
968 | 'test_float': 123.123,
969 | 'test_list': [
970 | 'test1',
971 | 'test2',
972 | 'test3'
973 | ],
974 | 'test_toml': {
975 | 'test_toml_key': 'test_toml_value'
976 | }
977 | }
978 | self.assertDictEqual(reply, expected_reply)
979 | reply = parser._parse_file_per_type("tml", test_files_location + "/test.toml")
980 | self.assertDictEqual(reply, expected_reply)
981 |
982 | def test_parser__parse_file_per_type_hcl(self):
983 | parser = ParseIt(config_location=test_files_location)
984 | reply = parser._parse_file_per_type("hcl", test_files_location + "/test.hcl")
985 | expected_reply = {
986 | 'file_type': 'hcl',
987 | 'test_string': 'testing',
988 | 'test_bool_true': True,
989 | 'test_bool_false': False,
990 | 'test_int': 123,
991 | 'test_float': 123.123,
992 | 'test_dict': {
993 | "hcl_dict_key": "hcl_dict_value"
994 | },
995 | 'test_list': [
996 | 'test1',
997 | 'test2',
998 | 'test3'
999 | ],
1000 | 'test_hcl': {
1001 | "test_hcl_name": {
1002 | 'test_hcl_key': 'test_hcl_value'
1003 | }
1004 | }
1005 | }
1006 | self.assertDictEqual(reply, expected_reply)
1007 | reply = parser._parse_file_per_type("tf", test_files_location + "/test.hcl")
1008 | self.assertDictEqual(reply, expected_reply)
1009 |
1010 | def test_parser__parse_file_per_type_ini(self):
1011 | parser = ParseIt(config_location=test_files_location)
1012 | reply = parser._parse_file_per_type("ini", test_files_location + "/test.ini")
1013 | expected_reply = {
1014 | 'DEFAULT': {
1015 | 'file_type': 'ini',
1016 | 'test_string': 'testing',
1017 | 'test_bool_true': 'true',
1018 | 'test_bool_false': 'false',
1019 | 'test_int': '123.0',
1020 | 'test_float': '123.123',
1021 | 'test_list': '["test1", "test2", "test3"]'
1022 | },
1023 | 'test_ini': {
1024 | 'test_ini_key': 'test_ini_value'
1025 | }
1026 | }
1027 | self.assertDictEqual(reply, expected_reply)
1028 | reply = parser._parse_file_per_type("conf", test_files_location + "/test.ini")
1029 | self.assertDictEqual(reply, expected_reply)
1030 | reply = parser._parse_file_per_type("cfg", test_files_location + "/test.ini")
1031 | self.assertDictEqual(reply, expected_reply)
1032 |
1033 | def test_parser__parse_file_per_type_xml(self):
1034 | parser = ParseIt(config_location=test_files_location)
1035 | reply = parser._parse_file_per_type("xml", test_files_location + "/test.xml")
1036 | expected_reply = {
1037 | 'xml_root': {
1038 | 'file_type': 'xml',
1039 | 'test_bool_false': 'false',
1040 | 'test_bool_true': 'true',
1041 | 'test_float': '123.123',
1042 | 'test_int': '123',
1043 | 'test_xml': {
1044 | 'test_xml_key': 'test_xml_value'
1045 | },
1046 | 'test_list': {
1047 | 'element': [
1048 | 'test1',
1049 | 'test2',
1050 | 'test3'
1051 | ]
1052 | },
1053 | 'test_string': 'testing'
1054 | }
1055 | }
1056 | self.assertDictEqual(reply, expected_reply)
1057 |
1058 | def test_command_line_args_read_command_line_arg(self):
1059 | test_args = ["parse_it_mock_script.py", "--test_key", "test_value"]
1060 | with mock.patch('sys.argv', test_args):
1061 | reply = read_command_line_arg("test_key")
1062 | self.assertEqual(reply, "test_value")
1063 |
1064 | def test_command_line_args_read_command_line_arg_not_defined(self):
1065 | test_args = ["parse_it_mock_script.py", "--test_key", "test_value"]
1066 | with mock.patch('sys.argv', test_args):
1067 | reply = read_command_line_arg("non_existing_test_key")
1068 | self.assertIsNone(reply)
1069 |
1070 | def test_command_line_args_command_line_arg_defined_false(self):
1071 | test_args = ["parse_it_mock_script.py", "--test_key", "test_value"]
1072 | with mock.patch('sys.argv', test_args):
1073 | reply = command_line_arg_defined("non_existing_test_key")
1074 | self.assertFalse(reply)
1075 |
1076 | def test_command_line_args_command_line_arg_defined_true(self):
1077 | test_args = ["parse_it_mock_script.py", "--test_key", "test_value"]
1078 | with mock.patch('sys.argv', test_args):
1079 | reply = command_line_arg_defined("test_key")
1080 | self.assertTrue(reply)
1081 |
1082 | def test_command_line_args_read_all_cli_args_to_dict(self):
1083 | test_args = ["parse_it_mock_script.py", "--test_key", "test_value", "--another_test", "another_value"]
1084 | expected_reply = {"test_key": "test_value", "another_test": "another_value"}
1085 | with mock.patch('sys.argv', test_args):
1086 | reply = read_all_cli_args_to_dict()
1087 | self.assertEqual(reply, expected_reply)
1088 |
1089 | def test_command_line_args_read_all_cli_args_to_dict_no_cli_args(self):
1090 | test_args = ["parse_it_mock_script.py"]
1091 | expected_reply = {}
1092 | with mock.patch('sys.argv', test_args):
1093 | reply = read_all_cli_args_to_dict()
1094 | self.assertEqual(reply, expected_reply)
1095 |
1096 | def test_command_line_args_read_all_cli_args_to_dict_no_cli_args_with_double_dash(self):
1097 | test_args = ["parse_it_mock_script.py", "-wrong_format_test_key", "test_value"]
1098 | expected_reply = {}
1099 | with mock.patch('sys.argv', test_args):
1100 | reply = read_all_cli_args_to_dict()
1101 | self.assertEqual(reply, expected_reply)
1102 |
1103 | def test_parser_read_configuration_from_cli_arg(self):
1104 | test_args = ["parse_it_mock_script.py", "--test_cli_key", "test_value", "--test_cli_int", "123"]
1105 | with mock.patch('sys.argv', test_args):
1106 | parser = ParseIt()
1107 | reply = parser.read_configuration_variable("test_cli_key")
1108 | self.assertEqual(reply, "test_value")
1109 | reply = parser.read_configuration_variable("test_cli_int")
1110 | self.assertEqual(reply, 123)
1111 |
1112 | def test_parser_read_configuration_variable_check_allowed_types_None(self):
1113 | parser = ParseIt(config_location=test_files_location)
1114 | reply_json = parser.read_configuration_variable("file_type", allowed_types=None)
1115 | self.assertEqual(reply_json, "env")
1116 |
1117 | def test_parser_read_configuration_variable_check_allowed_not_in_types_list(self):
1118 | with self.assertRaises(TypeError):
1119 | parser = ParseIt(config_location=test_files_location)
1120 | parser.read_configuration_variable("file_type", allowed_types=[int, list, dict, None, float])
1121 |
1122 | def test_parser_read_configuration_variable_check_allowed_in_types_list(self):
1123 | parser = ParseIt(config_location=test_files_location)
1124 | reply_json = parser.read_configuration_variable("file_type", allowed_types=[int, str, dict, None, float])
1125 | self.assertEqual(reply_json, "env")
1126 |
1127 | def test_parser_read_multiple_configuration_variables_works_multiple_list(self):
1128 | expected_reply = {
1129 | 'file_type': 'env',
1130 | 'test_hcl': {
1131 | 'test_hcl_name': {
1132 | 'test_hcl_key': 'test_hcl_value'
1133 | }
1134 | },
1135 | 'test_int': 123,
1136 | 'test_list': [
1137 | 'test1',
1138 | 'test2',
1139 | 'test3'
1140 | ]
1141 | }
1142 | parser = ParseIt(config_location=test_files_location)
1143 | reply_json = parser.read_multiple_configuration_variables(["file_type", "test_hcl", "test_int", "test_list"])
1144 | self.assertDictEqual(reply_json, expected_reply)
1145 |
1146 | def test_parser_read_multiple_configuration_variables_default_value(self):
1147 | expected_reply = {
1148 | 'file_type': 'env',
1149 | 'non_existing_value': 'test'
1150 | }
1151 | parser = ParseIt(config_location=test_files_location)
1152 | reply_json = parser.read_multiple_configuration_variables(["file_type", "non_existing_value"],
1153 | default_value="test")
1154 | self.assertDictEqual(reply_json, expected_reply)
1155 |
1156 | def test_parser_read_multiple_configuration_variables_required_true_raise_error(self):
1157 | parser = ParseIt(config_location=test_files_location)
1158 | with self.assertRaises(ValueError):
1159 | parser.read_multiple_configuration_variables(["file_type", "non_existing_value"], required=True)
1160 |
1161 | def test_parser_read_multiple_configuration_variables_allowed_types_raise_error(self):
1162 | parser = ParseIt(config_location=test_files_location)
1163 | with self.assertRaises(TypeError):
1164 | parser.read_multiple_configuration_variables(["file_type", "non_existing_value"], allowed_types=[int, dict])
1165 |
1166 | def test_parser_read_all_configuration_variables(self):
1167 | parser = ParseIt(config_location=test_files_location)
1168 | reply = parser.read_all_configuration_variables()
1169 | self.assertEqual(reply["file_type"], "env")
1170 | self.assertEqual(reply["test_string"], "testing")
1171 | self.assertTrue(reply["test_bool_true"])
1172 | self.assertFalse(reply["test_bool_false"])
1173 | self.assertEqual(reply["test_int"], 123)
1174 | self.assertEqual(reply["test_float"], 123.123)
1175 | self.assertDictEqual(reply["test_dict"], {'hcl_dict_key': 'hcl_dict_value'})
1176 | self.assertListEqual(reply["test_list"], ['test1', 'test2', 'test3'])
1177 |
1178 | def test_parser_read_all_configuration_variables_nest_envvars(self):
1179 | parser = ParseIt(config_location=test_files_location, envvar_divider="_", force_envvars_uppercase=True)
1180 | test_envvars = {"test_split_nesting1": "test123", "test_split_nesting2": "test456"}
1181 | with mock.patch.dict(os.environ, test_envvars):
1182 | reply = parser.read_all_configuration_variables()
1183 | self.assertDictEqual(reply["test"]["split"], {'nesting1': 'test123', 'nesting2': 'test456'})
1184 | self.assertEqual(reply["file_type"], "env")
1185 | self.assertEqual(reply["test_string"], "testing")
1186 | self.assertTrue(reply["test_bool_true"])
1187 | self.assertFalse(reply["test_bool_false"])
1188 | self.assertEqual(reply["test_int"], 123)
1189 | self.assertEqual(reply["test_float"], 123.123)
1190 | self.assertDictEqual(reply["test_dict"], {'hcl_dict_key': 'hcl_dict_value'})
1191 | self.assertListEqual(reply["test_list"], ['test1', 'test2', 'test3'])
1192 |
1193 | def test_parser_read_all_configuration_variables_default_value_given(self):
1194 | parser = ParseIt(config_location=test_files_location)
1195 | reply = parser.read_all_configuration_variables(default_value={"default_value_test": "it_works"})
1196 | self.assertEqual(reply["default_value_test"], "it_works")
1197 | self.assertEqual(reply["file_type"], "env")
1198 | self.assertEqual(reply["test_string"], "testing")
1199 | self.assertTrue(reply["test_bool_true"])
1200 | self.assertFalse(reply["test_bool_false"])
1201 | self.assertEqual(reply["test_int"], 123)
1202 | self.assertEqual(reply["test_float"], 123.123)
1203 | self.assertDictEqual(reply["test_dict"], {'hcl_dict_key': 'hcl_dict_value'})
1204 | self.assertListEqual(reply["test_list"], ['test1', 'test2', 'test3'])
1205 |
1206 | def test_parser_read_all_configuration_variables_none_default_value_given(self):
1207 | parser = ParseIt(config_location=test_files_location)
1208 | reply = parser.read_all_configuration_variables(default_value=None)
1209 | self.assertEqual(reply["file_type"], "env")
1210 | self.assertEqual(reply["test_string"], "testing")
1211 | self.assertTrue(reply["test_bool_true"])
1212 | self.assertFalse(reply["test_bool_false"])
1213 | self.assertEqual(reply["test_int"], 123)
1214 | self.assertEqual(reply["test_float"], 123.123)
1215 | self.assertDictEqual(reply["test_dict"], {'hcl_dict_key': 'hcl_dict_value'})
1216 | self.assertListEqual(reply["test_list"], ['test1', 'test2', 'test3'])
1217 |
1218 | def test_parser_read_all_configuration_variables_empty_default_value_given(self):
1219 | parser = ParseIt(config_location=test_files_location)
1220 | reply = parser.read_all_configuration_variables(default_value={})
1221 | self.assertEqual(reply["file_type"], "env")
1222 | self.assertEqual(reply["test_string"], "testing")
1223 | self.assertTrue(reply["test_bool_true"])
1224 | self.assertFalse(reply["test_bool_false"])
1225 | self.assertEqual(reply["test_int"], 123)
1226 | self.assertEqual(reply["test_float"], 123.123)
1227 | self.assertDictEqual(reply["test_dict"], {'hcl_dict_key': 'hcl_dict_value'})
1228 | self.assertListEqual(reply["test_list"], ['test1', 'test2', 'test3'])
1229 |
1230 | def test_parser_read_all_configuration_variables_required_given(self):
1231 | parser = ParseIt(config_location=test_files_location)
1232 | reply = parser.read_all_configuration_variables(required=["file_type", "test_int"])
1233 | self.assertEqual(reply["file_type"], "env")
1234 | self.assertEqual(reply["test_string"], "testing")
1235 | self.assertTrue(reply["test_bool_true"])
1236 | self.assertFalse(reply["test_bool_false"])
1237 | self.assertEqual(reply["test_int"], 123)
1238 | self.assertEqual(reply["test_float"], 123.123)
1239 | self.assertDictEqual(reply["test_dict"], {'hcl_dict_key': 'hcl_dict_value'})
1240 | self.assertListEqual(reply["test_list"], ['test1', 'test2', 'test3'])
1241 |
1242 | def test_parser_read_all_configuration_variables_empty_required_given(self):
1243 | parser = ParseIt(config_location=test_files_location)
1244 | reply = parser.read_all_configuration_variables(required=[])
1245 | self.assertEqual(reply["file_type"], "env")
1246 | self.assertEqual(reply["test_string"], "testing")
1247 | self.assertTrue(reply["test_bool_true"])
1248 | self.assertFalse(reply["test_bool_false"])
1249 | self.assertEqual(reply["test_int"], 123)
1250 | self.assertEqual(reply["test_float"], 123.123)
1251 | self.assertDictEqual(reply["test_dict"], {'hcl_dict_key': 'hcl_dict_value'})
1252 | self.assertListEqual(reply["test_list"], ['test1', 'test2', 'test3'])
1253 |
1254 | def test_parser_read_all_configuration_variables_none_required_given(self):
1255 | parser = ParseIt(config_location=test_files_location)
1256 | reply = parser.read_all_configuration_variables(required=None)
1257 | self.assertEqual(reply["file_type"], "env")
1258 | self.assertEqual(reply["test_string"], "testing")
1259 | self.assertTrue(reply["test_bool_true"])
1260 | self.assertFalse(reply["test_bool_false"])
1261 | self.assertEqual(reply["test_int"], 123)
1262 | self.assertEqual(reply["test_float"], 123.123)
1263 | self.assertDictEqual(reply["test_dict"], {'hcl_dict_key': 'hcl_dict_value'})
1264 | self.assertListEqual(reply["test_list"], ['test1', 'test2', 'test3'])
1265 |
1266 | def test_parser_read_all_configuration_variables_allowed_types_given(self):
1267 | parser = ParseIt(config_location=test_files_location)
1268 | reply = parser.read_all_configuration_variables(allowed_types={
1269 | "file_type": [
1270 | str,
1271 | bool
1272 | ],
1273 | "test_int": [
1274 | int,
1275 | str
1276 | ]
1277 | })
1278 | self.assertEqual(reply["file_type"], "env")
1279 | self.assertEqual(reply["test_string"], "testing")
1280 | self.assertTrue(reply["test_bool_true"])
1281 | self.assertFalse(reply["test_bool_false"])
1282 | self.assertEqual(reply["test_int"], 123)
1283 | self.assertEqual(reply["test_float"], 123.123)
1284 | self.assertDictEqual(reply["test_dict"], {'hcl_dict_key': 'hcl_dict_value'})
1285 | self.assertListEqual(reply["test_list"], ['test1', 'test2', 'test3'])
1286 |
1287 | def test_parser_read_all_configuration_variables_empty_allowed_types_given(self):
1288 | parser = ParseIt(config_location=test_files_location)
1289 | reply = parser.read_all_configuration_variables(allowed_types={})
1290 | self.assertEqual(reply["file_type"], "env")
1291 | self.assertEqual(reply["test_string"], "testing")
1292 | self.assertTrue(reply["test_bool_true"])
1293 | self.assertFalse(reply["test_bool_false"])
1294 | self.assertEqual(reply["test_int"], 123)
1295 | self.assertEqual(reply["test_float"], 123.123)
1296 | self.assertDictEqual(reply["test_dict"], {'hcl_dict_key': 'hcl_dict_value'})
1297 | self.assertListEqual(reply["test_list"], ['test1', 'test2', 'test3'])
1298 |
1299 | def test_parser_read_all_configuration_variables_none_allowed_types_given(self):
1300 | parser = ParseIt(config_location=test_files_location)
1301 | reply = parser.read_all_configuration_variables(allowed_types=None)
1302 | self.assertEqual(reply["file_type"], "env")
1303 | self.assertEqual(reply["test_string"], "testing")
1304 | self.assertTrue(reply["test_bool_true"])
1305 | self.assertFalse(reply["test_bool_false"])
1306 | self.assertEqual(reply["test_int"], 123)
1307 | self.assertEqual(reply["test_float"], 123.123)
1308 | self.assertDictEqual(reply["test_dict"], {'hcl_dict_key': 'hcl_dict_value'})
1309 | self.assertListEqual(reply["test_list"], ['test1', 'test2', 'test3'])
1310 |
1311 | def test_parser_read_all_configuration_variables_type_estimate_true(self):
1312 | test_envvars = {"TEST_ENV_TYPE_ESTIMATE": "123"}
1313 | with mock.patch.dict(os.environ, test_envvars):
1314 | parser = ParseIt(config_location=test_files_location, type_estimate=True)
1315 | reply = parser.read_all_configuration_variables()
1316 | self.assertEqual(reply["test_env_type_estimate"], 123)
1317 |
1318 | def test_parser_read_all_configuration_variables_type_estimate_false(self):
1319 | test_envvars = {"TEST_ENV_TYPE_ESTIMATE": "123"}
1320 | with mock.patch.dict(os.environ, test_envvars):
1321 | parser = ParseIt(config_location=test_files_location, type_estimate=False)
1322 | reply = parser.read_all_configuration_variables()
1323 | self.assertEqual(reply["test_env_type_estimate"], "123")
1324 |
1325 | def test_parser_read_all_configuration_variables_raise_allowed_types_error(self):
1326 | parser = ParseIt(config_location=test_files_location)
1327 | with self.assertRaises(TypeError):
1328 | parser.read_all_configuration_variables(allowed_types={"file_type": [bool, dict]})
1329 |
1330 | def test_parser_read_all_configuration_variables_raise_required_error(self):
1331 | parser = ParseIt(config_location=test_files_location)
1332 | with self.assertRaises(ValueError):
1333 | parser.read_all_configuration_variables(required=["non_existing_key"])
1334 |
1335 | def test_parser_read_all_configuration_variables_config_type_in_data_sources_error(self):
1336 | parser = ParseIt(config_type_priority=['non_existing_type', 'json'])
1337 | with self.assertRaises(ValueError):
1338 | parser.read_all_configuration_variables(required=["non_existing_key"])
1339 |
1340 | def test_parser_read_all_configuration_variables_read_envvars(self):
1341 | test_envvars = {"TEST_ENV_TYPE_ESTIMATE": "123"}
1342 | with mock.patch.dict(os.environ, test_envvars):
1343 | parser = ParseIt(config_location=test_files_location, type_estimate=True)
1344 | reply = parser.read_all_configuration_variables()
1345 | self.assertEqual(reply["test_env_type_estimate"], 123)
1346 | self.assertEqual(reply["file_type"], "env")
1347 | self.assertEqual(reply["test_string"], "testing")
1348 | self.assertTrue(reply["test_bool_true"])
1349 | self.assertFalse(reply["test_bool_false"])
1350 | self.assertEqual(reply["test_int"], 123)
1351 | self.assertEqual(reply["test_float"], 123.123)
1352 | self.assertDictEqual(reply["test_dict"], {'hcl_dict_key': 'hcl_dict_value'})
1353 | self.assertListEqual(reply["test_list"], ['test1', 'test2', 'test3'])
1354 |
1355 | def test_parser_read_all_configuration_variables_read_cli_args(self):
1356 | test_args = ["parse_it_mock_script.py", "--test_cli_key_no_folder", "test_value"]
1357 | with mock.patch('sys.argv', test_args):
1358 | parser = ParseIt(config_location=test_files_location, type_estimate=True)
1359 | reply = parser.read_all_configuration_variables()
1360 | self.assertEqual(reply["test_cli_key_no_folder"], "test_value")
1361 | self.assertEqual(reply["file_type"], "env")
1362 | self.assertEqual(reply["test_string"], "testing")
1363 | self.assertTrue(reply["test_bool_true"])
1364 | self.assertFalse(reply["test_bool_false"])
1365 | self.assertEqual(reply["test_int"], 123)
1366 | self.assertEqual(reply["test_float"], 123.123)
1367 | self.assertDictEqual(reply["test_dict"], {'hcl_dict_key': 'hcl_dict_value'})
1368 | self.assertListEqual(reply["test_list"], ['test1', 'test2', 'test3'])
1369 |
1370 | def test_parser_read_all_configuration_variables_read_single_file(self):
1371 | parser = ParseIt(config_location=test_files_location + "/test.hcl", type_estimate=True)
1372 | reply = parser.read_all_configuration_variables()
1373 | self.assertEqual(reply["file_type"], "hcl")
1374 | self.assertEqual(reply["test_string"], "testing")
1375 | self.assertTrue(reply["test_bool_true"])
1376 | self.assertFalse(reply["test_bool_false"])
1377 | self.assertEqual(reply["test_int"], 123)
1378 | self.assertEqual(reply["test_float"], 123.123)
1379 | self.assertDictEqual(reply["test_dict"], {'hcl_dict_key': 'hcl_dict_value'})
1380 | self.assertListEqual(reply["test_list"], ['test1', 'test2', 'test3'])
1381 |
1382 | def test_parser_read_all_configuration_variables_recursive_false(self):
1383 | parser = ParseIt(config_location=test_files_location, recurse=False)
1384 | reply = parser.read_all_configuration_variables()
1385 | self.assertEqual(reply["file_type"], "env")
1386 | self.assertEqual(reply["test_string"], "testing")
1387 | self.assertTrue(reply["test_bool_true"])
1388 | self.assertFalse(reply["test_bool_false"])
1389 | self.assertEqual(reply["test_int"], 123)
1390 | self.assertEqual(reply["test_float"], 123.123)
1391 | self.assertDictEqual(reply["test_dict"], {'hcl_dict_key': 'hcl_dict_value'})
1392 | self.assertListEqual(reply["test_list"], ['test1', 'test2', 'test3'])
1393 |
1394 | def test_parser_read_all_configuration_variables_recursive_true(self):
1395 | parser = ParseIt(config_location=test_files_location, recurse=True)
1396 | reply = parser.read_all_configuration_variables()
1397 | self.assertEqual(reply["file_type"], "env")
1398 | self.assertEqual(reply["test_string"], "testing")
1399 | self.assertTrue(reply["test_bool_true"])
1400 | self.assertTrue(reply["test_json_subfolder"])
1401 | self.assertFalse(reply["test_bool_false"])
1402 | self.assertEqual(reply["test_int"], 123)
1403 | self.assertEqual(reply["test_float"], 123.123)
1404 | self.assertDictEqual(reply["test_dict"], {'hcl_dict_key': 'hcl_dict_value'})
1405 | self.assertListEqual(reply["test_list"], ['test1', 'test2', 'test3'])
1406 |
1407 | def test_parser_read_all_configuration_variables_check_order(self):
1408 | parser = ParseIt(config_location=test_files_location, config_type_priority=["env_vars", "hcl", "json"])
1409 | test_envvars = {"TEST_INT": "12345"}
1410 | with mock.patch.dict(os.environ, test_envvars):
1411 | reply = parser.read_all_configuration_variables()
1412 | self.assertEqual(reply["file_type"], "hcl")
1413 | self.assertEqual(reply["test_string"], "testing")
1414 | self.assertTrue(reply["test_bool_true"])
1415 | self.assertFalse(reply["test_bool_false"])
1416 | self.assertEqual(reply["test_int"], 12345)
1417 | self.assertEqual(reply["test_float"], 123.123)
1418 | self.assertDictEqual(reply["test_dict"], {'hcl_dict_key': 'hcl_dict_value'})
1419 | self.assertListEqual(reply["test_list"], ['test1', 'test2', 'test3'])
1420 | self.assertDictEqual(reply["test_json"], {"test_json_key": "test_json_value"})
1421 |
1422 | def test_parser_type_estimate_none(self):
1423 | parser = ParseIt(
1424 | config_location=f"{test_files_location}/test_none_values.json")
1425 | reply = parser.read_all_configuration_variables()
1426 | self.assertEqual(reply["empty_string"], None)
1427 | self.assertEqual(reply["none_lower"], None)
1428 | self.assertEqual(reply["none_upper"], None)
1429 | self.assertEqual(reply["none_mixed"], None)
1430 | self.assertEqual(reply["null"], None)
1431 |
1432 | def test_parser_type_estimate_none_custom(self):
1433 | parser = ParseIt(
1434 | config_location=f"{test_files_location}/test_none_values.json",
1435 | none_values={"null", "none"})
1436 | reply = parser.read_all_configuration_variables()
1437 | self.assertNotEqual(reply["empty_string"], None)
1438 | self.assertEqual(reply["none_lower"], None)
1439 | self.assertEqual(reply["none_upper"], None)
1440 | self.assertEqual(reply["none_mixed"], None)
1441 | self.assertEqual(reply["null"], None)
1442 |
--------------------------------------------------------------------------------