├── 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: [![CI/CD](https://github.com/naorlivne/parse_it/actions/workflows/full_ci_cd_workflow.yml/badge.svg)](https://github.com/naorlivne/parse_it/actions/workflows/full_ci_cd_workflow.yml) 6 | 7 | Code coverage: [![codecov](https://codecov.io/gh/naorlivne/parse_it/branch/master/graph/badge.svg)](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 | --------------------------------------------------------------------------------