├── .editorconfig ├── .github ├── ISSUE_TEMPLATE.md └── gconfigs-logo.png ├── .gitignore ├── .travis.yml ├── AUTHORS.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.rst ├── Dockerfile ├── HISTORY.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── docker-compose.yml ├── gconfigs ├── __init__.py ├── backends.py └── gconfigs.py ├── poetry.lock ├── pyproject.toml ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── files ├── config-files │ ├── .env │ └── .env-empty └── configs │ └── CONFIG_TEST ├── test_backends.py └── test_gconfigs_api.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [LICENSE] 14 | insert_final_newline = false 15 | 16 | [Makefile] 17 | indent_style = tab 18 | 19 | [*.{yml}] 20 | indent_style = space 21 | indent_size = 2 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * gConfigs version: 2 | * Python version: 3 | * Operating System: 4 | 5 | ### Description 6 | 7 | Describe what you were trying to get done. 8 | Tell us what happened, what went wrong, and what you expected to happen. 9 | 10 | ### What I Did 11 | 12 | ``` 13 | Paste the command(s) you ran and the output. 14 | If there was a crash, please include the traceback here. 15 | ``` 16 | -------------------------------------------------------------------------------- /.github/gconfigs-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/douglasmiranda/gconfigs/3cfca5700ee51994420ed87752e93e79afe65c50/.github/gconfigs-logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | sdist/ 19 | wheels/ 20 | *.egg-info/ 21 | .installed.cfg 22 | *.egg 23 | 24 | # Unit test / coverage reports 25 | htmlcov/ 26 | .coverage 27 | .coverage.* 28 | .cache 29 | nosetests.xml 30 | coverage.xml 31 | *.cover 32 | .hypothesis/ 33 | 34 | # pyenv 35 | .python-version 36 | 37 | # virtualenv 38 | .venv 39 | venv/ 40 | ENV/ 41 | 42 | tmp/ 43 | .vscode/ 44 | pip-wheel-metadata/ 45 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Config file for automatic testing at travis-ci.org 2 | 3 | dist: xenial 4 | language: python 5 | python: 6 | - 3.6 7 | - 3.7 8 | - 3.8-dev 9 | matrix: 10 | allow_failures: 11 | - python: 3.8-dev 12 | 13 | install: pip install poetry; poetry install 14 | 15 | script: poetry run coverage run --source gconfigs -m pytest tests/ 16 | after_success: 17 | - coveralls 18 | notifications: 19 | on_success: change 20 | on_failure: always 21 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # Credits 2 | 3 | ## Development Lead 4 | 5 | * Douglas Miranda 6 | 7 | ## Contributors 8 | 9 | None yet. Why not be the first? 10 | -------------------------------------------------------------------------------- /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 at douglascoding@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 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: shell 2 | 3 | ============ 4 | Contributing 5 | ============ 6 | 7 | Contributions are welcome, and they are greatly appreciated! Every little bit 8 | helps, and credit will always be given. 9 | 10 | You can contribute in many ways: 11 | 12 | Types of Contributions 13 | ---------------------- 14 | 15 | Report Bugs 16 | ~~~~~~~~~~~ 17 | 18 | Report bugs at https://github.com/douglasmiranda/gconfigs/issues. 19 | 20 | If you are reporting a bug, please include: 21 | 22 | * Your operating system name and version. 23 | * Any details about your local setup that might be helpful in troubleshooting. 24 | * Detailed steps to reproduce the bug. 25 | 26 | Fix Bugs 27 | ~~~~~~~~ 28 | 29 | Look through the GitHub issues for bugs. Anything tagged with "bug" and "help 30 | wanted" is open to whoever wants to implement it. 31 | 32 | Implement Features 33 | ~~~~~~~~~~~~~~~~~~ 34 | 35 | Look through the GitHub issues for features. Anything tagged with "enhancement" 36 | and "help wanted" is open to whoever wants to implement it. 37 | 38 | Write Documentation 39 | ~~~~~~~~~~~~~~~~~~~ 40 | 41 | gConfigs could always use more documentation, whether as part of the 42 | official gConfigs docs, in docstrings, or even on the web in blog posts, 43 | articles, and such. 44 | 45 | Submit Feedback 46 | ~~~~~~~~~~~~~~~ 47 | 48 | The best way to send feedback is to file an issue at https://github.com/douglasmiranda/gconfigs/issues. 49 | 50 | If you are proposing a feature: 51 | 52 | * Explain in detail how it would work. 53 | * Keep the scope as narrow as possible, to make it easier to implement. 54 | * Remember that this is a volunteer-driven project, and that contributions 55 | are welcome :) 56 | 57 | 58 | Get Started! 59 | ------------ 60 | 61 | Ready to contribute? Here's how to set up `gConfigs` for local development. 62 | 63 | 1. Fork the `gconfigs` repo on GitHub. 64 | 2. Clone your fork locally:: 65 | 66 | $ git clone git@github.com:your_name_here/gconfigs.git 67 | 68 | 3. After you enter into your local copy directory and just install the dev dependencies. You can see those dependencies in Pipfile and Pipfile.lock (freezed versions):: 69 | 70 | $ cd gconfigs/ 71 | $ pipenv install --dev 72 | 73 | 4. Create a branch for local development:: 74 | 75 | $ git checkout -b name-of-your-bugfix-or-feature 76 | 77 | Now you can make your changes locally. 78 | 79 | 5. When you're done making changes, make sure everything is formated properly, 80 | check that your changes pass flake8 and the tests:: 81 | 82 | $ make lint # this will use black to show you the diff of the formatted code 83 | $ make format # this will use black to format the code 84 | $ make test 85 | $ make coverage # this will show you if you're not missing something on tests. 86 | $ # Or you could just: 87 | $ make format 88 | $ make full_test 89 | 90 | 6. Commit your changes and push your branch to GitHub:: 91 | 92 | $ git add . 93 | $ git commit -m "Your detailed description of your changes." 94 | $ git push origin name-of-your-bugfix-or-feature 95 | 96 | 7. Submit a pull request through the GitHub website. 97 | 98 | 99 | Pull Request Guidelines 100 | ----------------------- 101 | 102 | Before you submit a pull request, check that it meets these guidelines: 103 | 104 | 1. The pull request should include tests. 105 | 2. If the pull request adds functionality, the docs should be updated. Put 106 | your new functionality into a function with a docstring, and add the 107 | feature to the list in README.rst. 108 | 3. The pull request should work for Python 3.6. Check 109 | https://travis-ci.org/douglasmiranda/gconfigs/pull_requests 110 | and make sure that the tests pass for all supported Python versions. 111 | 112 | 113 | Tips 114 | ---- 115 | 116 | To run a subset of tests:: 117 | 118 | $ py.test tests.test_gconfigs_api 119 | 120 | 121 | Deploying 122 | --------- 123 | 124 | A reminder for the maintainers on how to deploy. 125 | Make sure all your changes are committed (including an entry in HISTORY.rst). 126 | Then run:: 127 | 128 | $ pipenv run bumpversion patch # possible: major / minor / patch 129 | $ git push 130 | $ git push --tags 131 | $ make dist 132 | $ make release 133 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6-alpine3.7 2 | 3 | RUN apk add --no-cache make \ 4 | && addgroup -S gconfigs && adduser -S -G gconfigs gconfigs 5 | 6 | WORKDIR /gconfigs 7 | 8 | RUN pip install pipenv 9 | 10 | USER gconfigs 11 | 12 | COPY Pipfile ./ 13 | COPY Pipfile.lock ./ 14 | 15 | RUN pipenv install --dev 16 | 17 | COPY . ./ 18 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # History 2 | 3 | 4 | ## v0.1.5 (2019-04-10) 5 | 6 | - Don't raise exception when the dotenv file is empty #3 7 | - New `use_instead` option. #4 [docs](https://github.com/douglasmiranda/gconfigs#id11) 8 | - Added CODE OF CONDUCT! 9 | - Dev: Using Poetry; Some travis changes; Some updates to Makefile; 10 | 11 | ## v0.1.4 (2018-04-16) 12 | 13 | - Just added some testing 14 | 15 | ## v0.1.3 (2018-04-15) 16 | 17 | - Fix wrong bumpversion stuff from previous release 18 | - Fix information about release CONTRIBUTING 19 | 20 | 21 | ## v0.1.2 (2018-04-15) 22 | 23 | - Mostly documentation and dev setup patches 24 | - Fix failing test on travis 25 | 26 | 27 | ## v0.1.1 (2018-04-09) 28 | 29 | - Open sourcing the project 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018, Douglas Miranda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CONTRIBUTING.rst 3 | include HISTORY.rst 4 | include LICENSE 5 | include README.rst 6 | 7 | recursive-include tests * 8 | recursive-exclude * __pycache__ 9 | recursive-exclude * *.py[co] 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-test clean-pyc clean-build help 2 | .DEFAULT_GOAL := help 3 | 4 | help: ## show make targets 5 | @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {sub("\\\\n",sprintf("\n%22c"," "), $$2);printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) 6 | 7 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts 8 | 9 | clean-build: ## remove build artifacts 10 | rm -fr build/ 11 | rm -fr dist/ 12 | rm -fr .eggs/ 13 | find . -name '*.egg-info' -exec rm -fr {} + 14 | find . -name '*.egg' -exec rm -f {} + 15 | 16 | clean-pyc: ## remove Python file artifacts 17 | find . -name '*.pyc' -exec rm -f {} + 18 | find . -name '*.pyo' -exec rm -f {} + 19 | find . -name '*~' -exec rm -f {} + 20 | find . -name '__pycache__' -exec rm -fr {} + 21 | 22 | clean-test: ## remove test and coverage artifacts 23 | rm -f .coverage 24 | rm -fr htmlcov/ 25 | rm -fr .pytest_cache/ 26 | 27 | format: ## format code using black (https://github.com/ambv/black) 28 | poetry run black gconfigs/ tests/ 29 | 30 | lint: ## use black to show diff of formatted code 31 | poetry run black --diff gconfigs/ tests/ 32 | 33 | test: ## run tests quickly with the default Python 34 | poetry run pytest gconfigs tests/ -s 35 | 36 | coverage: ## run tests quickly with the default Python 37 | poetry run coverage run --source gconfigs -m pytest tests/ 38 | poetry run coverage report -m 39 | 40 | full_test: ## check style, run tests and show coverage reports 41 | @printf "\033[34m$1Step 1 - Cheking Code Style With Black. (If there's something that could look better you will see the diff)\033[0m\n" 42 | @$(MAKE) lint 43 | @printf "\033[34m$1Step 2 - Running Tests. (Wait for coverage report)\033[0m\n" 44 | @$(MAKE) coverage 45 | @printf "\033[34m$1Step 3 - Cleaning Coverage Files\033[0m\n" 46 | @$(MAKE) clean-test 47 | 48 | release: clean ## package and upload a release 49 | poetry run python setup.py sdist upload 50 | poetry run python setup.py bdist_wheel upload 51 | 52 | dist: clean ## builds source and wheel package 53 | poetry run python setup.py sdist 54 | poetry run python setup.py bdist_wheel 55 | ls -l dist 56 | 57 | readme: ## make readme with rst2html.py, output: ./tmp/output.html 58 | poetry run python setup.py --long-description | poetry run rst2html.py - > ./tmp/output.html 59 | 60 | install: clean ## install the package to the active Python's site-packages 61 | poetry run python setup.py install 62 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ################################################# 2 | Config and Secret parser for your Python projects 3 | ################################################# 4 | 5 | .. image:: https://github.com/douglasmiranda/gconfigs/blob/master/.github/gconfigs-logo.png?raw=true 6 | :alt: gConfigs - Config and Secret parser for your Python projects 7 | :target: https://github.com/douglasmiranda/gconfigs 8 | 9 | | 10 | 11 | .. image:: https://img.shields.io/pypi/v/gconfigs.svg 12 | :alt: Badge Version 13 | :target: https://pypi.python.org/pypi/gconfigs 14 | 15 | .. image:: https://img.shields.io/travis/douglasmiranda/gconfigs.svg 16 | :alt: Badge Travis Build 17 | :target: https://travis-ci.org/douglasmiranda/gconfigs 18 | 19 | .. image:: https://coveralls.io/repos/github/douglasmiranda/gconfigs/badge.svg 20 | :alt: Badge Coveralls - Coverage Status 21 | :target: https://coveralls.io/github/douglasmiranda/gconfigs 22 | 23 | .. image:: https://img.shields.io/badge/code%20style-black-000000.svg 24 | :alt: Code style: black 25 | :target: https://github.com/ambv/black 26 | 27 | .. image:: https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg 28 | :alt: Just Say Thanks if you like gConfigs 29 | :target: https://saythanks.io/to/douglasmiranda 30 | 31 | | 32 | 33 | Let me show you some code: 34 | 35 | .. code-block:: python 36 | 37 | from gconfigs import envs, configs, secrets 38 | 39 | HOME = envs('HOME', default='/') 40 | DEBUG = configs.as_bool('DEBUG', default=False) 41 | DATABASE_USER = configs('DATABASE_USER') 42 | DATABASE_PASS = secrets('DATABASE_PASS') 43 | 44 | 45 | .. code-block:: pycon 46 | 47 | >>> # envs, configs and secrets are iterables 48 | >>> from gconfigs import envs 49 | >>> for env in envs: 50 | ... print(env) 51 | ... print(env.key) 52 | ... print(env.value) 53 | ... 54 | EnvironmentVariable(key='ENV_TEST', value='env-test-1') 55 | ENV_TEST 56 | env-test-1 57 | ... 58 | 59 | >>> 'ENV_TEST' in envs 60 | True 61 | 62 | >>> envs.json() 63 | '{"ENV_TEST": "env-test-1", "HOME": "/root", ...}' 64 | 65 | 66 | **This is experimental, so you know, use at your own risk.** 67 | 68 | .. contents:: **Table of Contents** 69 | :local: 70 | 71 | 72 | Features 73 | ******** 74 | 75 | * Python 3.6 76 | * No dependencies 77 | 78 | Read configs from: 79 | 80 | * `Environment Variables`_ 81 | * `Local Mounted Configs and Secrets`_ 82 | * `.env (dotenv) files`_ 83 | 84 | 85 | Installation 86 | ************ 87 | 88 | To install gConfigs, run this command in your terminal: 89 | 90 | .. code-block:: console 91 | 92 | $ pip install gconfigs 93 | $ # or 94 | $ pipenv install gconfigs 95 | 96 | These are the preferred methods to install gConfigs. 97 | 98 | If you don't have `pip`_ or `pipenv`_ installed, this `Python installation guide`_ can guide you through the process. 99 | 100 | .. _pip: https://pip.pypa.io 101 | .. _pipenv: https://docs.pipenv.org 102 | .. _Python installation guide: http://docs.python-guide.org/en/latest/starting/installation/ 103 | 104 | 105 | Usage - Basics 106 | ************** 107 | 108 | I will show you the basics with the built-in backends. 109 | 110 | I'm still deciding about other backends. If you need a custom backend, it's easy to create. Check "Advanced" section for more. 111 | 112 | 113 | Environment Variables 114 | ===================== 115 | 116 | So, there are good reasons to **not** use environment variables for your configs, but if you want / need to use, please just not use for sensitive data, like: passwords, secret keys, private tokens and stuff like that. 117 | 118 | .. code-block:: pycon 119 | 120 | >>> from gconfigs import envs 121 | # contents from ``envs`` are just data from ``os.environ`` 122 | >>> envs 123 | 124 | >>> envs('HOME') 125 | '/root' 126 | 127 | 128 | Local Mounted Configs and Secrets 129 | ================================= 130 | 131 | Configs and secrets can be mounted as text files, read-only and in a secure location if possible, and we can read its contents. Basically the file name will be like a var / key name and its contents will be the value. 132 | 133 | 134 | configs 135 | ------- 136 | 137 | For ``configs``, *gConfigs* will look for mouted files at **/run/configs**, for example:: 138 | 139 | File Absolute Path: /run/configs/LANGUAGE_CODE 140 | File Name: LANGUAGE_CODE 141 | File Contents: en-us 142 | 143 | .. code-block:: python 144 | 145 | from gconfigs import configs 146 | LANGUAGE_CODE = configs('LANGUAGE_CODE') 147 | # ...translates into: 148 | LANGUAGE_CODE = "en-us" 149 | 150 | Of course you can change the path that *gConfigs* will look for your configs. Let's suppose your configs are mouted at **/configs**: 151 | 152 | .. code-block:: python 153 | 154 | from gconfigs import configs 155 | configs.root_dir = '/configs' 156 | # will look for /configs/LANGUAGE_CODE 157 | LANGUAGE_CODE = configs('LANGUAGE_CODE') 158 | 159 | This is the simplest way to do it. Check section "Advanced" for more. 160 | 161 | 162 | secrets 163 | ------- 164 | 165 | It follows the same flow as ``configs``, so for more details go to ``configs``. 166 | 167 | For ``secrets``, *gConfigs* will look for mouted files at **/run/secrets**. 168 | 169 | .. code-block:: python 170 | 171 | from gconfigs import secrets 172 | POSTGRES_PASSWORD = secrets('POSTGRES_PASSWORD') 173 | # ...translates into: 174 | POSTGRES_PASSWORD = "super-strong-password" 175 | secrets.root_dir = '/secrets' 176 | # will look for /secrets/POSTGRES_PASSWORD 177 | POSTGRES_PASSWORD = secrets('POSTGRES_PASSWORD') 178 | 179 | **NOTE:** If you don't know what tools follow these workflows for configurations and secrets, you could try with `Docker`_. Check `Docker Configs`_ and / or `Docker Secrets`_ management with Docker. 180 | 181 | .. _Docker: https://www.docker.com/ 182 | 183 | 184 | .env (dotenv) files 185 | =================== 186 | 187 | .env files are present not only in Python projects, for that reason many developers are familiar with, it's just like a .ini file, but without the sections, you could say it's a key-value store in a file. 188 | 189 | .env files could be a good solution depending on your stack. It's better than environment variables at least. 190 | 191 | You could just put your configurations in a file called **.env**, (or whatever name you want), for example the contents of your file would be: 192 | 193 | .. code-block:: INI 194 | 195 | ROOT=/ 196 | PROJECT_NAME=gConfigs - Config and Secret parser 197 | AUTH_MODULE=users.User 198 | 199 | After that I'm going to save my **.env** file in **/app/**, then the full path will be **/app/.env**, now let's see how to load all it's contents in *gConfigs*: 200 | 201 | .. code-block:: python 202 | 203 | from gconfigs import dotenvs 204 | dotenvs.load_file('/app/.env') 205 | # after that it's like using ``envs``, or ``configs`` 206 | ROOT = dotenvs('ROOT') 207 | NAME = dotenvs('PROJECT_NAME') 208 | AUTH = dotenvs('AUTH_MODULE') 209 | 210 | NOTES: 211 | * if it's a .ini syntax it will be parsed, but it will ignore sections 212 | * duplicated keys will be overridden by the latest value 213 | * inexistent keys will raise exception 214 | * all values load as strings, use casting to convert them 215 | * didn't like the name ``dotenvs``? Just do: ``from gconfigs import dotenvs as configs`` 216 | 217 | 218 | Usage - Advanced 219 | **************** 220 | 221 | With the basics, you are already running your projects just fine, but if you want the extra stuff of *gConfigs*, I'll show you. 222 | 223 | I'll be using envs in the examples, but it should work for all built-in backends. 224 | 225 | 226 | Get Your Config Value 227 | ===================== 228 | 229 | use another key if the first one doesn't exist 230 | ---------------------------------------------- 231 | 232 | Use another key in case the first doesn't exist. It's like a default value but instead of a value, you use the `use_instead` 233 | parameter to inform a key to be used when `key` is not found. 234 | 235 | .. code-block:: pycon 236 | 237 | >>> from gconfigs import envs 238 | >>> envs('NON-EXISTENT-ENV', use_instead='USE-THIS-ENV-INSTEAD') 239 | '/' 240 | >>> user_or_host = envs('USER', use_instead='HOSTNAME') 241 | 242 | 243 | default value 244 | ------------- 245 | 246 | You can provide a default value, in case the backend couldn't return the config. 247 | 248 | .. code-block:: pycon 249 | 250 | >>> from gconfigs import envs 251 | >>> envs('WHATEVER', default='/') 252 | '/' 253 | 254 | use_instead + default 255 | --------------------- 256 | 257 | It's simple, if both `key` and `use_instead` doesn't exist, the `default` value will be used. 258 | 259 | .. code-block:: pycon 260 | 261 | >>> from gconfigs import envs 262 | >>> envs('NON-EXISTENT-ENV', use_instead='NON-EXISTENT-ENV-2', default='hello') 263 | 'hello' 264 | 265 | typed value 266 | ----------- 267 | 268 | Generally backends will return key and value as strings, but you can return other types. 269 | 270 | ``gconfigs.GConfigs.get`` won't try to cast your typed value. 271 | 272 | For example when providing a ``default`` value you could set a ``int``: 273 | 274 | .. code-block:: pycon 275 | 276 | >>> from gconfigs import envs 277 | >>> envs('WORKERS', default=1) 278 | 1 279 | 280 | But you **must** know that if your backend, in that case it's just the ``LocalEnv`` backend, return a string value, you could create a bug in your configuration. Unless your software is prepared to deal with the number of ``WORKERS`` being a string and an integer, you could be in trouble. 281 | 282 | What you want here is to cast your value, that you could achieve by simply converting what gConfigs return to the desired type or using some of the built-in casting methods. 283 | 284 | 285 | casting (converting your strings to a specific type) 286 | ---------------------------------------------------- 287 | 288 | Most of the backends will return a string (``str``) as value. But sometimes you want to use a ``bool``, ``int``, ``list`` config. 289 | 290 | **NOTE:** I choose to **not** do too much magic, so the cast methods implemented for *gConfigs* just loads the values with ``json.loads`` from the Python's built-in ``json`` module. Therefore, it must be a valid json value, I'll show you how: 291 | 292 | 293 | BOOLEAN - Converting to bool 294 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 295 | 296 | Let's say you want ``DEBUG`` as a boolean. 297 | 298 | .. code-block:: pycon 299 | 300 | >>> from gconfigs import envs 301 | >>> envs.as_bool('DEBUG') 302 | True 303 | 304 | I'm not doing any magic translation of ``"on"`` => ``True`` | ``"yes"`` => ``True``. I don't want to introduce ambiguity, In my opinion, configurations must be straightforward and with limited variations. 305 | 306 | 307 | LIST - Converting to list 308 | ^^^^^^^^^^^^^^^^^^^^^^^^^ 309 | 310 | Let's say you have a configuration value like this: 311 | 312 | .. code-block:: bash 313 | 314 | [1, 2.1, "string-value", true] 315 | 316 | # if you want to try in your terminal: 317 | export CONFIG_LIST='[1, 2.1, "string-value", true]' 318 | 319 | The value must be just JSON-like, which is very close to a list in Python. And you will be able to get a list object by doing: 320 | 321 | .. code-block:: pycon 322 | 323 | >>> from gconfigs import envs 324 | >>> envs.as_list('CONFIG-LIST') 325 | [1, 2.1, 'string-value', True] 326 | 327 | 328 | DICT - Converting to dict 329 | ^^^^^^^^^^^^^^^^^^^^^^^^^ 330 | 331 | If you have a value that is basically a JSON valid object, you may already know you can turn into a ``dict`` using ``json.loads``. 332 | 333 | Here is an example, if your config value is: 334 | 335 | .. code-block:: bash 336 | 337 | {"endpoint": "/", "workers": 1, "debug": true} 338 | 339 | # if you want to try in your terminal: 340 | export CONFIG_DICT='{"endpoint": "/", "workers": 1, "debug": true}' 341 | 342 | 343 | .. code-block:: pycon 344 | 345 | >>> from gconfigs import envs 346 | >>> envs.as_list('CONFIG-LIST') 347 | {'endpoint': '/', 'workers': 1, 'debug': True} 348 | 349 | Again, nothing new, no surprises, boring, no magic... as intended. 350 | 351 | 352 | OTHER TYPES - Converting to int, float, tuple, str, set 353 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 354 | 355 | Well let's not reinvent the wheel, like I said before, most backends will return string by default, so if we have something like: 356 | 357 | .. code-block:: bash 358 | 359 | WORKERS="1" 360 | WEIGHT="1.1" 361 | MODULES='["auth", "session"]' 362 | 363 | We could then do this: 364 | 365 | .. code-block:: pycon 366 | 367 | >>> from gconfigs import envs 368 | >>> int(envs('WORKERS')) 369 | 1 370 | >>> float(envs('WEIGHT')) 371 | 1.1 372 | 373 | If you want ``tuple`` or ``set``, just get as list and then do whatever you want: 374 | 375 | .. code-block:: pycon 376 | 377 | >>> from gconfigs import envs 378 | >>> tuple(envs.as_list('MODULES')) 379 | ('auth', 'session') 380 | >>> set(envs.as_list('MODULES')) 381 | {'auth', 'session'} 382 | 383 | What about strings? If you getting from your backend config values that aren't strings, and for some of them you need to convert to ``str``, just use the Python built-in ``str()``: 384 | 385 | .. code-block:: pycon 386 | 387 | >>> from gconfigs import envs 388 | >>> envs('AN-INT-CONFIG') # if this return an integer 389 | 1 390 | >>> str(envs('AN-INT-CONFIG')) # just use str 391 | '1' 392 | 393 | 394 | Interators 395 | ========== 396 | 397 | .. code-block:: pycon 398 | 399 | >>> from gconfigs import envs 400 | >>> list(envs) # envs is a iterator 401 | [EnvironmentVariable(key='LANG', value='C.UTF-8'), ...] 402 | 403 | >>> for env in envs: 404 | ... print(env) 405 | ... print(env.key) 406 | ... print(env.value) 407 | ... 408 | EnvironmentVariable(key='ENV_TEST', value='env-test-1') 409 | ENV_TEST 410 | env-test-1 411 | ... 412 | 413 | If you use an iterator once, you can't iterate again, but if you want you can call `.iterator()` and get a new one: 414 | 415 | .. code-block:: pycon 416 | 417 | >>> iter_envs = envs.iterator() 418 | >>> for env in iter_envs: 419 | ... print(env.key) 420 | ... 421 | HOME 422 | LANG 423 | 424 | 425 | Extra Goodies 426 | ============= 427 | 428 | * How many configs with Python built-in ``len``. 429 | * Config key exists with Python built-in ``in``. 430 | * Output your key-value configs as JSON. 431 | 432 | .. code-block:: pycon 433 | 434 | >>> from gconfigs import envs 435 | >>> len(envs) 436 | 28 437 | >>> 'HOME' in envs 438 | True 439 | >>> envs.json() 440 | '{"HOME": "/root", ...}' 441 | 442 | 443 | Beyond: from gconfigs import envs 444 | ********************************* 445 | 446 | Let's see some stuff you can do more than just import the ready for use ``configs`` and ``secrets``. 447 | 448 | We have ``GConfigs`` class which takes data from one of the backends ``gconfigs.backends`` and and add fancy stuff like casting and iterator behaviour. 449 | 450 | A backend is simply a class implementing the methods: 451 | 452 | * ``get(key: str)``: return a value given a key 453 | * ``keys()``: return all available keys 454 | 455 | If you know some Python, just look the ``gconfigs.backends.LocalEnv`` and you'll see there's no secret. 456 | 457 | 458 | Extending a Built-in Backend 459 | ============================ 460 | 461 | Okay let's create a practical example of how to override the behaviour of one of our backends. 462 | 463 | If you get your Configs and Secrets with ``gconfigs.configs`` and ``gconfigs.secrets``, you are making use of ``gconfigs.LocalMountFile`` backend. That being said we could extend ``gconfigs.LocalMountFile`` and make it only get the configs if they are a *mount point*. 464 | 465 | .. code-block:: python 466 | 467 | from gconfigs import GConfigs, LocalMountFile 468 | import os 469 | 470 | class MountPointConfigs(LocalMountFile): 471 | def get(self, key, **kwargs): 472 | file = self.root_dir / key 473 | if os.path.ismount(file): 474 | return super().get(key, **kwargs) 475 | 476 | raise Exception(f"The config {key} file must be a mount point.") 477 | 478 | # :backend: can be a callable class or a instance 479 | # :object_type_name: it's just the name of the namedtuple you get when you 480 | # iterate over `configs`. 481 | configs = GConfigs( 482 | backend=MountPointConfigs, object_type_name="MountPointConfig" 483 | ) 484 | 485 | MY_CONFIG = configs('MY_CONFIG') 486 | 487 | (if you use `Docker Configs`_ or `Docker Secrets`_, you probably know that it does mount your configs / secrets in your container filesystem) 488 | 489 | 490 | Create Your Own Backend 491 | ======================= 492 | 493 | If you want to extend the usage of *gConfigs* with other backends, it's not a hard task. 494 | 495 | Imagine my configs are stored in Redis (a key-value store), a backend for this would look like: 496 | 497 | .. code-block:: python 498 | 499 | class RedisBackend: 500 | """Redis Backend for gConfigs 501 | NOTE: this is an example, so you probably would have to install the "redis" 502 | python package, then connect to Redis, then you would be able to implement 503 | ``get`` and ``keys`` methods. 504 | """ 505 | def keys(self): 506 | # return a iterable of all keys available 507 | return available_keys 508 | 509 | def get(self, key: str): 510 | # this method receive a key (identifier of a config) 511 | # and return its respective value 512 | return value 513 | 514 | *gConfigs* only expects you provide two methods: 515 | 516 | ``get(key: str)``: return a value given a key 517 | * connect to your backend 518 | * based on the ``key`` get it's value 519 | * return the value OR raise exception if it was not possible to get the config 520 | * keep in mind that the return type it's up to you, ``str`` makes things kinda agnostic 521 | 522 | ``keys()``: return all available keys 523 | * connect to your backend 524 | * return an iterable (list, tuple, generator..) of all available keys if possible 525 | * if you don't want or it's not possible to implement this, just raise a ``NotImplementedError`` or a more informative exception if you like 526 | 527 | (Optional) ``load_file(filepath: str)``: parse file and just raise exception if fails 528 | * IMPORTANT: the method name it has to be ``load_file``, that way gConfigs will provide a ``load_file`` that just calls the backend to load the file, check ``gconfigs.GConfigs.__init__`` for more 529 | * read the file 530 | * parse and get keys and values 531 | * store the keys and values inside a ``dict`` if you want 532 | * then implement ``get`` and ``keys`` as described above 533 | 534 | You could also look at the module ``gconfigs.backends``, so you can see how the built-in backends are implemented. 535 | 536 | 537 | What's Next 538 | *********** 539 | 540 | * More backends, the really fun ones 541 | * Don't know, you tell me on `Issues`_ 542 | 543 | .. _Docker Configs: https://docs.docker.com/engine/swarm/configs/ 544 | .. _Docker Secrets: https://docs.docker.com/engine/swarm/secrets/ 545 | 546 | .. _Issues: https://github.com/douglasmiranda/gconfigs/issues 547 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.5" 2 | 3 | services: 4 | # docker-compose run --rm gconfigs 5 | gconfigs: 6 | build: ./ 7 | volumes: 8 | - "./:/gconfigs" 9 | command: 10 | - make 11 | - full_test 12 | -------------------------------------------------------------------------------- /gconfigs/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | gConfigs - Config and Secret parser 5 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 6 | 7 | Nothing shows better than some snippets: 8 | 9 | from gconfigs import envs, configs, secrets 10 | 11 | HOME = envs('HOME') 12 | DEBUG = configs.as_bool('DEBUG', default=False) 13 | DATABASE_USER = configs('DATABASE_USER') 14 | DATABASE_PASS = secrets('DATABASE_PASS') 15 | 16 | >>> # envs, configs and secrets are iterables 17 | >>> from gconfigs import envs 18 | >>> for env in envs: 19 | ... print(env) 20 | ... print(env.key) 21 | ... print(env.value) 22 | ... 23 | EnvironmentVariable(key='ENV_TEST', value='env-test-1') 24 | ENV_TEST 25 | env-test-1 26 | ... 27 | 28 | >>> 'ENV_TEST' in envs 29 | True 30 | 31 | >>> envs.json() 32 | '{"ENV_TEST": "env-test-1", "HOME": "/root", ...}' 33 | """ 34 | 35 | __author__ = """Douglas Miranda""" 36 | __email__ = "douglascoding@gmail.com" 37 | __version__ = "0.1.4" 38 | 39 | 40 | from .gconfigs import GConfigs 41 | from .backends import LocalEnv, DotEnv, LocalMountFile 42 | 43 | 44 | envs = GConfigs(backend=LocalEnv, object_type_name="EnvironmentVariable") 45 | dotenvs = GConfigs(backend=DotEnv, object_type_name="DotEnvConfig") 46 | configs = GConfigs( 47 | backend=LocalMountFile(root_dir="/run/configs"), object_type_name="Config" 48 | ) 49 | secrets = GConfigs( 50 | backend=LocalMountFile(root_dir="/run/secrets"), object_type_name="Secret" 51 | ) 52 | -------------------------------------------------------------------------------- /gconfigs/backends.py: -------------------------------------------------------------------------------- 1 | """ 2 | Backends for gConfigs 3 | ~~~~~~~~~~~~~~~~~~~~~ 4 | 5 | Backends are just simple classes with at least two methods implemented: 6 | `get` and `keys`. 7 | 8 | Example: 9 | 10 | class RedisBackend: 11 | def keys(self): 12 | # return a iterable of all keys available 13 | return available_keys 14 | 15 | def get(self, key: str): 16 | # this method receive a key (identifier of a config) 17 | # and return its respective value 18 | return value 19 | 20 | Notes: 21 | - If it's not possible to provide a `.keys` method, just declare with: 22 | raise NotImplementedError. Of course it will limit the goodies of gConfigs. 23 | - For errors on `.get` method just throw exceptions. 24 | (Config doesn't exists, you don't have permission, stuff like that) 25 | See `GConfigs.get` and you'll see that it has a `default` parameter, 26 | and of course if you provide a default value it will not throw a exception. 27 | """ 28 | from pathlib import Path 29 | import os 30 | 31 | 32 | class LocalEnv: 33 | def keys(self): 34 | return os.environ.keys() 35 | 36 | def get(self, key, **kwargs): 37 | value = os.environ.get(key) 38 | if value is None: 39 | raise KeyError( 40 | f"Environment variable '{key}' not set. Check for any " 41 | "misconfiguration or misspelling of the variable name." 42 | ) 43 | 44 | return value 45 | 46 | 47 | class LocalMountFile: 48 | def __init__(self, root_dir="/", pattern="*"): 49 | self.pattern = pattern 50 | self.root_dir = root_dir 51 | 52 | @property 53 | def root_dir(self): 54 | if not self._root_dir.exists(): 55 | raise RootDirectoryNotFound( 56 | f"The root directory {self._root_dir} doesn't exist." 57 | ) 58 | 59 | return self._root_dir 60 | 61 | @root_dir.setter 62 | def root_dir(self, root_dir): 63 | self._root_dir = Path(root_dir) 64 | 65 | def keys(self): 66 | for item in self.root_dir.glob(self.pattern): 67 | if item.is_file(): 68 | yield item.name 69 | 70 | def get(self, key, **kwargs): 71 | file = self.root_dir / key 72 | if file.exists(): 73 | return file.read_text() 74 | 75 | raise FileNotFoundError( 76 | f"Check if your files are mounted on {self.root_dir}. " 77 | "And remember to check if your system is case sensitive." 78 | ) 79 | 80 | 81 | class DotEnv: 82 | def keys(self): 83 | return self._data.keys() 84 | 85 | def get(self, key, **kwargs): 86 | if not hasattr(self, "_dotenv_file"): 87 | raise Exception("It seems like you didn't loaded the your dotenv file yet.") 88 | 89 | value = self._data.get(key) 90 | if value is None: 91 | raise KeyError( 92 | f"The config '{key}' is not set on {self._dotenv_file}. Check " 93 | "for any misconfiguration or misspelling of the variable name." 94 | ) 95 | 96 | return value 97 | 98 | def load_file(self, filepath): 99 | self._dotenv_file = filepath 100 | self._data = {} 101 | with open(self._dotenv_file) as file: 102 | for line in file.readlines(): 103 | # ignore comments, section title or invalid lines 104 | if line.startswith(("#", ";", "[")) or "=" not in line: 105 | continue 106 | 107 | # split on the first =, allows for subsequent `=` in strings 108 | key, value = line.split("=", 1) 109 | key = key.strip() 110 | 111 | if not (key and value): 112 | continue 113 | 114 | self._data[key] = value.rstrip("\r\n") 115 | 116 | 117 | class RootDirectoryNotFound(FileNotFoundError): 118 | pass 119 | -------------------------------------------------------------------------------- /gconfigs/gconfigs.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from collections import namedtuple 3 | from functools import lru_cache 4 | import json 5 | 6 | 7 | class NoValue: 8 | def __repr__(self): # pragma: no cover 9 | return f"<{self.__class__.__name__}>" 10 | 11 | 12 | NOTSET = NoValue() 13 | 14 | 15 | class GConfigs: 16 | def __init__(self, backend, strip=True, object_type_name="KeyValue"): 17 | """ 18 | :param backend: Backend / parser of configs. A simple class implementing `get` and `keys` methods. `gconfigs.backends` for more information. 19 | :param strip: Control the stripping of return value of `self.get` method. 20 | :param object_type_name: Simply a nice name for our key value named tuple. 21 | """ 22 | if hasattr(backend, "get") or hasattr(backend, "keys"): 23 | self._backend = backend() if callable(backend) else backend 24 | else: 25 | raise AttributeError( 26 | "'backend' class must have at least the methods 'get' and 'keys'." 27 | ) 28 | 29 | self.strip = strip 30 | self.object_type_name = object_type_name 31 | 32 | if hasattr(self._backend, "load_file") and callable(self._backend.load_file): 33 | self.load_file = self._load_file 34 | 35 | self._iter_configs = self.iterator() 36 | 37 | def get(self, key, default=NOTSET, use_instead=NOTSET, strip=None, **kwargs): 38 | """Return value for given key. 39 | :param var: Key (Name) of config. 40 | :param default: If backend doesn't return valid config, return this instead. 41 | :param use_instead: If `key` doesn't exist use the alternative key `use_instead`. 42 | :param strip: Control the stripping of return value. Override the default `self.strip` with `True` or `False`. Will strip if is a string value. 43 | 44 | :returns: Parsed value or default. Or raises exceptions you implement in your backend. 45 | """ 46 | 47 | try: 48 | value = self._backend.get(key) 49 | # This may seem a generic try/except but I'm actually catching the 50 | # specific Exception that you will implement in your backend. 51 | except Exception as e: 52 | if use_instead is not NOTSET: 53 | return self.get(use_instead, default, NOTSET, strip, **kwargs) 54 | 55 | if default is NOTSET: 56 | raise e 57 | 58 | value = default 59 | 60 | strip_ = self.strip if strip is None else strip 61 | if strip_ and isinstance(value, str): 62 | return value.strip() 63 | 64 | return value 65 | 66 | def as_bool(self, key, **kwargs): 67 | value = self.get(key, **kwargs) 68 | if isinstance(value, bool): 69 | return value 70 | 71 | if isinstance(value, str) and value.lower() in ("true", "false"): 72 | return self._cast(value.lower()) 73 | 74 | raise ValueError(f"Could not cast the value '{value}' to boolean.") 75 | 76 | def as_list(self, key, **kwargs): 77 | value = self.get(key, **kwargs) 78 | if isinstance(value, list) or isinstance(value, tuple): 79 | return list(value) 80 | 81 | if isinstance(value, str) and value.startswith("[") and value.endswith("]"): 82 | return self._cast(value) 83 | 84 | raise ValueError(f"Could not cast the value '{value}' to list.") 85 | 86 | def as_dict(self, key, **kwargs): 87 | value = self.get(key, **kwargs) 88 | if isinstance(value, dict): 89 | return value 90 | 91 | if isinstance(value, str) and value.startswith("{") and value.endswith("}"): 92 | return self._cast(value) 93 | 94 | raise ValueError(f"Could not cast the value '{value}' to dict.") 95 | 96 | def _cast(self, value): 97 | try: 98 | return json.loads(value) 99 | 100 | except json.decoder.JSONDecodeError as e: 101 | raise ValueError( 102 | f"Could not cast the value {value}. Tried to cast with json module, so it must be a valid json value." 103 | ) 104 | 105 | def json(self): 106 | """Returns json parsed data of all available data. 107 | """ 108 | return json.dumps({item.key: item.value for item in self.iterator()}) 109 | 110 | def _load_file(self, filepath): 111 | self._backend.load_file(filepath) 112 | 113 | @property 114 | @lru_cache() 115 | def _cached_namedtuple(self): 116 | return namedtuple(self.object_type_name, ["key", "value"]) 117 | 118 | def iterator(self): 119 | for key in self._backend.keys(): 120 | yield self._cached_namedtuple(key, self.get(key)) 121 | 122 | def __next__(self): 123 | return next(self._iter_configs) 124 | 125 | def __iter__(self): 126 | return self 127 | 128 | def __call__(self, key, **kwargs): 129 | return self.get(key, **kwargs) 130 | 131 | def __contains__(self, key): 132 | return key in self._backend.keys() 133 | 134 | def __len__(self): 135 | return len(self._backend.keys()) 136 | 137 | def __repr__(self): # pragma: no cover 138 | return f"" 139 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | category = "dev" 3 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 4 | name = "appdirs" 5 | optional = false 6 | python-versions = "*" 7 | version = "1.4.3" 8 | 9 | [[package]] 10 | category = "dev" 11 | description = "Atomic file writes." 12 | name = "atomicwrites" 13 | optional = false 14 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 15 | version = "1.3.0" 16 | 17 | [[package]] 18 | category = "dev" 19 | description = "Classes Without Boilerplate" 20 | name = "attrs" 21 | optional = false 22 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 23 | version = "19.1.0" 24 | 25 | [[package]] 26 | category = "dev" 27 | description = "The uncompromising code formatter." 28 | name = "black" 29 | optional = false 30 | python-versions = ">=3.6" 31 | version = "19.3b0" 32 | 33 | [package.dependencies] 34 | appdirs = "*" 35 | attrs = ">=18.1.0" 36 | click = ">=6.5" 37 | toml = ">=0.9.4" 38 | 39 | [[package]] 40 | category = "dev" 41 | description = "An easy safelist-based HTML-sanitizing tool." 42 | name = "bleach" 43 | optional = false 44 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 45 | version = "3.1.0" 46 | 47 | [package.dependencies] 48 | six = ">=1.9.0" 49 | webencodings = "*" 50 | 51 | [[package]] 52 | category = "dev" 53 | description = "Version-bump your software with a single command!" 54 | name = "bumpversion" 55 | optional = false 56 | python-versions = "*" 57 | version = "0.5.3" 58 | 59 | [[package]] 60 | category = "dev" 61 | description = "Python package for providing Mozilla's CA Bundle." 62 | name = "certifi" 63 | optional = false 64 | python-versions = "*" 65 | version = "2019.3.9" 66 | 67 | [[package]] 68 | category = "dev" 69 | description = "Universal encoding detector for Python 2 and 3" 70 | name = "chardet" 71 | optional = false 72 | python-versions = "*" 73 | version = "3.0.4" 74 | 75 | [[package]] 76 | category = "dev" 77 | description = "Composable command line interface toolkit" 78 | name = "click" 79 | optional = false 80 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 81 | version = "7.0" 82 | 83 | [[package]] 84 | category = "dev" 85 | description = "Cross-platform colored terminal text." 86 | marker = "sys_platform == \"win32\"" 87 | name = "colorama" 88 | optional = false 89 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 90 | version = "0.4.1" 91 | 92 | [[package]] 93 | category = "dev" 94 | description = "Code coverage measurement for Python" 95 | name = "coverage" 96 | optional = false 97 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4" 98 | version = "4.5.3" 99 | 100 | [[package]] 101 | category = "dev" 102 | description = "Show coverage stats online via coveralls.io" 103 | name = "coveralls" 104 | optional = false 105 | python-versions = "*" 106 | version = "1.7.0" 107 | 108 | [package.dependencies] 109 | coverage = ">=3.6" 110 | docopt = ">=0.6.1" 111 | requests = ">=1.0.0" 112 | 113 | [[package]] 114 | category = "dev" 115 | description = "Pythonic argument parser, that will make you smile" 116 | name = "docopt" 117 | optional = false 118 | python-versions = "*" 119 | version = "0.6.2" 120 | 121 | [[package]] 122 | category = "dev" 123 | description = "Docutils -- Python Documentation Utilities" 124 | name = "docutils" 125 | optional = false 126 | python-versions = "*" 127 | version = "0.14" 128 | 129 | [[package]] 130 | category = "dev" 131 | description = "Internationalized Domain Names in Applications (IDNA)" 132 | name = "idna" 133 | optional = false 134 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 135 | version = "2.8" 136 | 137 | [[package]] 138 | category = "dev" 139 | description = "More routines for operating on iterables, beyond itertools" 140 | marker = "python_version > \"2.7\"" 141 | name = "more-itertools" 142 | optional = false 143 | python-versions = ">=3.4" 144 | version = "7.0.0" 145 | 146 | [[package]] 147 | category = "dev" 148 | description = "Query metadatdata from sdists / bdists / installed packages." 149 | name = "pkginfo" 150 | optional = false 151 | python-versions = "*" 152 | version = "1.5.0.1" 153 | 154 | [[package]] 155 | category = "dev" 156 | description = "plugin and hook calling mechanisms for python" 157 | name = "pluggy" 158 | optional = false 159 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 160 | version = "0.9.0" 161 | 162 | [[package]] 163 | category = "dev" 164 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 165 | name = "py" 166 | optional = false 167 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 168 | version = "1.8.0" 169 | 170 | [[package]] 171 | category = "dev" 172 | description = "Pygments is a syntax highlighting package written in Python." 173 | name = "pygments" 174 | optional = false 175 | python-versions = "*" 176 | version = "2.3.1" 177 | 178 | [[package]] 179 | category = "dev" 180 | description = "pytest: simple powerful testing with Python" 181 | name = "pytest" 182 | optional = false 183 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 184 | version = "4.4.0" 185 | 186 | [package.dependencies] 187 | atomicwrites = ">=1.0" 188 | attrs = ">=17.4.0" 189 | colorama = "*" 190 | pluggy = ">=0.9" 191 | py = ">=1.5.0" 192 | setuptools = "*" 193 | six = ">=1.10.0" 194 | 195 | [package.dependencies.more-itertools] 196 | python = ">2.7" 197 | version = ">=4.0.0" 198 | 199 | [[package]] 200 | category = "dev" 201 | description = "readme_renderer is a library for rendering \"readme\" descriptions for Warehouse" 202 | name = "readme-renderer" 203 | optional = false 204 | python-versions = "*" 205 | version = "24.0" 206 | 207 | [package.dependencies] 208 | Pygments = "*" 209 | bleach = ">=2.1.0" 210 | docutils = ">=0.13.1" 211 | six = "*" 212 | 213 | [[package]] 214 | category = "dev" 215 | description = "Python HTTP for Humans." 216 | name = "requests" 217 | optional = false 218 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 219 | version = "2.21.0" 220 | 221 | [package.dependencies] 222 | certifi = ">=2017.4.17" 223 | chardet = ">=3.0.2,<3.1.0" 224 | idna = ">=2.5,<2.9" 225 | urllib3 = ">=1.21.1,<1.25" 226 | 227 | [[package]] 228 | category = "dev" 229 | description = "A utility belt for advanced users of python-requests" 230 | name = "requests-toolbelt" 231 | optional = false 232 | python-versions = "*" 233 | version = "0.9.1" 234 | 235 | [package.dependencies] 236 | requests = ">=2.0.1,<3.0.0" 237 | 238 | [[package]] 239 | category = "dev" 240 | description = "Simple command runner" 241 | name = "runner" 242 | optional = false 243 | python-versions = "*" 244 | version = "1.1" 245 | 246 | [[package]] 247 | category = "dev" 248 | description = "Python 2 and 3 compatibility utilities" 249 | name = "six" 250 | optional = false 251 | python-versions = ">=2.6, !=3.0.*, !=3.1.*" 252 | version = "1.12.0" 253 | 254 | [[package]] 255 | category = "dev" 256 | description = "Python Library for Tom's Obvious, Minimal Language" 257 | name = "toml" 258 | optional = false 259 | python-versions = "*" 260 | version = "0.10.0" 261 | 262 | [[package]] 263 | category = "dev" 264 | description = "Fast, Extensible Progress Meter" 265 | name = "tqdm" 266 | optional = false 267 | python-versions = ">=2.6, !=3.0.*, !=3.1.*" 268 | version = "4.31.1" 269 | 270 | [[package]] 271 | category = "dev" 272 | description = "Collection of utilities for publishing packages on PyPI" 273 | name = "twine" 274 | optional = false 275 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 276 | version = "1.13.0" 277 | 278 | [package.dependencies] 279 | pkginfo = ">=1.4.2" 280 | readme-renderer = ">=21.0" 281 | requests = ">=2.5.0,<2.15 || >2.15,<2.16 || >2.16" 282 | requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0" 283 | setuptools = ">=0.7.0" 284 | tqdm = ">=4.14" 285 | 286 | [[package]] 287 | category = "dev" 288 | description = "HTTP library with thread-safe connection pooling, file post, and more." 289 | name = "urllib3" 290 | optional = false 291 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" 292 | version = "1.24.1" 293 | 294 | [[package]] 295 | category = "dev" 296 | description = "Character encoding aliases for legacy web content" 297 | name = "webencodings" 298 | optional = false 299 | python-versions = "*" 300 | version = "0.5.1" 301 | 302 | [metadata] 303 | content-hash = "0bdecfc21f78fa562741e6f34685d0ea215ee4cfb9ca2a4ecf11c6ff15f86726" 304 | python-versions = "^3.6" 305 | 306 | [metadata.hashes] 307 | appdirs = ["9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", "d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"] 308 | atomicwrites = ["03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", "75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"] 309 | attrs = ["69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", "f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"] 310 | black = ["09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf", "68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c"] 311 | bleach = ["213336e49e102af26d9cde77dd2d0397afabc5a6bf2fed985dc35b5d1e285a16", "3fdf7f77adcf649c9911387df51254b813185e32b2c6619f690b593a617e19fa"] 312 | bumpversion = ["6744c873dd7aafc24453d8b6a1a0d6d109faf63cd0cd19cb78fd46e74932c77e", "6753d9ff3552013e2130f7bc03c1007e24473b4835952679653fb132367bdd57"] 313 | certifi = ["59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5", "b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae"] 314 | chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"] 315 | click = ["2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"] 316 | colorama = ["05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"] 317 | coverage = ["0c5fe441b9cfdab64719f24e9684502a59432df7570521563d7b1aff27ac755f", "2b412abc4c7d6e019ce7c27cbc229783035eef6d5401695dccba80f481be4eb3", "3684fabf6b87a369017756b551cef29e505cb155ddb892a7a29277b978da88b9", "39e088da9b284f1bd17c750ac672103779f7954ce6125fd4382134ac8d152d74", "3c205bc11cc4fcc57b761c2da73b9b72a59f8d5ca89979afb0c1c6f9e53c7390", "42692db854d13c6c5e9541b6ffe0fe921fe16c9c446358d642ccae1462582d3b", "465ce53a8c0f3a7950dfb836438442f833cf6663d407f37d8c52fe7b6e56d7e8", "48020e343fc40f72a442c8a1334284620f81295256a6b6ca6d8aa1350c763bbe", "4ec30ade438d1711562f3786bea33a9da6107414aed60a5daa974d50a8c2c351", "5296fc86ab612ec12394565c500b412a43b328b3907c0d14358950d06fd83baf", "5f61bed2f7d9b6a9ab935150a6b23d7f84b8055524e7be7715b6513f3328138e", "6899797ac384b239ce1926f3cb86ffc19996f6fa3a1efbb23cb49e0c12d8c18c", "68a43a9f9f83693ce0414d17e019daee7ab3f7113a70c79a3dd4c2f704e4d741", "6b8033d47fe22506856fe450470ccb1d8ba1ffb8463494a15cfc96392a288c09", "7ad7536066b28863e5835e8cfeaa794b7fe352d99a8cded9f43d1161be8e9fbd", "7bacb89ccf4bedb30b277e96e4cc68cd1369ca6841bde7b005191b54d3dd1034", "839dc7c36501254e14331bcb98b27002aa415e4af7ea039d9009409b9d2d5420", "8e679d1bde5e2de4a909efb071f14b472a678b788904440779d2c449c0355b27", "8f9a95b66969cdea53ec992ecea5406c5bd99c9221f539bca1e8406b200ae98c", "932c03d2d565f75961ba1d3cec41ddde00e162c5b46d03f7423edcb807734eab", "93f965415cc51604f571e491f280cff0f5be35895b4eb5e55b47ae90c02a497b", "988529edadc49039d205e0aa6ce049c5ccda4acb2d6c3c5c550c17e8c02c05ba", "998d7e73548fe395eeb294495a04d38942edb66d1fa61eb70418871bc621227e", "9de60893fb447d1e797f6bf08fdf0dbcda0c1e34c1b06c92bd3a363c0ea8c609", "9e80d45d0c7fcee54e22771db7f1b0b126fb4a6c0a2e5afa72f66827207ff2f2", "a545a3dfe5082dc8e8c3eb7f8a2cf4f2870902ff1860bd99b6198cfd1f9d1f49", "a5d8f29e5ec661143621a8f4de51adfb300d7a476224156a39a392254f70687b", "a9abc8c480e103dc05d9b332c6cc9fb1586330356fc14f1aa9c0ca5745097d19", "aca06bfba4759bbdb09bf52ebb15ae20268ee1f6747417837926fae990ebc41d", "bb23b7a6fd666e551a3094ab896a57809e010059540ad20acbeec03a154224ce", "bfd1d0ae7e292105f29d7deaa9d8f2916ed8553ab9d5f39ec65bcf5deadff3f9", "c22ab9f96cbaff05c6a84e20ec856383d27eae09e511d3e6ac4479489195861d", "c62ca0a38958f541a73cf86acdab020c2091631c137bd359c4f5bddde7b75fd4", "c709d8bda72cf4cd348ccec2a4881f2c5848fd72903c185f363d361b2737f773", "c968a6aa7e0b56ecbd28531ddf439c2ec103610d3e2bf3b75b813304f8cb7723", "ca58eba39c68010d7e87a823f22a081b5290e3e3c64714aac3c91481d8b34d22", "df785d8cb80539d0b55fd47183264b7002077859028dfe3070cf6359bf8b2d9c", "f406628ca51e0ae90ae76ea8398677a921b36f0bd71aab2099dfed08abd0322f", "f46087bbd95ebae244a0eda01a618aff11ec7a069b15a3ef8f6b520db523dcf1", "f8019c5279eb32360ca03e9fac40a12667715546eed5c5eb59eb381f2f501260", "fc5f4d209733750afd2714e9109816a29500718b32dd9a5db01c0cb3a019b96a"] 318 | coveralls = ["baa26648430d5c2225ab12d7e2067f75597a4b967034bba7e3d5ab7501d207a1", "ff9b7823b15070f26f654837bb02a201d006baaf2083e0514ffd3b34a3ffed81"] 319 | docopt = ["49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"] 320 | docutils = ["02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", "51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", "7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"] 321 | idna = ["c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", "ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"] 322 | more-itertools = ["2112d2ca570bb7c3e53ea1a35cd5df42bb0fd10c45f0fb97178679c3c03d64c7", "c3e4748ba1aad8dba30a4886b0b1a2004f9a863837b8654e7059eebf727afa5a"] 323 | pkginfo = ["7424f2c8511c186cd5424bbf31045b77435b37a8d604990b79d4e70d741148bb", "a6d9e40ca61ad3ebd0b72fbadd4fba16e4c0e4df0428c041e01e06eb6ee71f32"] 324 | pluggy = ["19ecf9ce9db2fce065a7a0586e07cfb4ac8614fe96edf628a264b1c70116cf8f", "84d306a647cc805219916e62aab89caa97a33a1dd8c342e87a37f91073cd4746"] 325 | py = ["64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", "dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"] 326 | pygments = ["5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a", "e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d"] 327 | pytest = ["13c5e9fb5ec5179995e9357111ab089af350d788cbc944c628f3cde72285809b", "f21d2f1fb8200830dcbb5d8ec466a9c9120e20d8b53c7585d180125cce1d297a"] 328 | readme-renderer = ["bb16f55b259f27f75f640acf5e00cf897845a8b3e4731b5c1a436e4b8529202f", "c8532b79afc0375a85f10433eca157d6b50f7d6990f337fa498c96cd4bfc203d"] 329 | requests = ["502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", "7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"] 330 | requests-toolbelt = ["380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f", "968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"] 331 | runner = ["8cc81514f88eecb914d847bfcf3280ded74d461516ccdfd3e6802d320cc00b6f"] 332 | six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"] 333 | toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"] 334 | tqdm = ["d385c95361699e5cf7622485d9b9eae2d4864b21cd5a2374a9c381ffed701021", "e22977e3ebe961f72362f6ddfb9197cc531c9737aaf5f607ef09740c849ecd05"] 335 | twine = ["0fb0bfa3df4f62076cab5def36b1a71a2e4acb4d1fa5c97475b048117b1a6446", "d6c29c933ecfc74e9b1d9fa13aa1f87c5d5770e119f5a4ce032092f0ff5b14dc"] 336 | urllib3 = ["61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", "de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"] 337 | webencodings = ["a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", "b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"] 338 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "gconfigs" 3 | version = "0.1.4" 4 | description = "Config and Secret parser for your Python projects" 5 | authors = ["Douglas Miranda "] 6 | license = "MIT" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.6" 10 | 11 | [tool.poetry.dev-dependencies] 12 | black = "=19.3b0" 13 | bumpversion = "^0.5.3" 14 | coverage = "^4.5" 15 | twine = "^1.13" 16 | pytest = "^4.4" 17 | runner = "^1.1" 18 | coveralls = "^1.7" 19 | pygments = "^2.3" 20 | 21 | [build-system] 22 | requires = ["poetry>=0.12"] 23 | build-backend = "poetry.masonry.api" 24 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.1.4 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:pyproject.toml] 7 | search = version = "{current_version}" 8 | replace = version = "{new_version}" 9 | 10 | [bumpversion:file:setup.py] 11 | search = version="{current_version}" 12 | replace = version="{new_version}" 13 | 14 | [bumpversion:file:gconfigs/__init__.py] 15 | search = __version__ = "{current_version}" 16 | replace = __version__ = "{new_version}" 17 | 18 | [bdist_wheel] 19 | universal = 1 20 | 21 | [flake8] 22 | exclude = docs 23 | ignore = E501 24 | 25 | [aliases] 26 | test = pipenv run pytest gconfigs tests/ -s 27 | 28 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """The setup script.""" 5 | 6 | from setuptools import setup, find_packages 7 | import sys 8 | 9 | assert sys.version_info >= (3, 6, 0), "black requires Python 3.6+" 10 | 11 | from pathlib import Path 12 | 13 | readme = Path("README.rst").read_text() 14 | history = Path("HISTORY.rst").read_text() 15 | 16 | setup( 17 | name="gconfigs", 18 | version="0.1.4", 19 | description="gConfigs - Config and Secret parser for your Python projects.", 20 | long_description=f"{readme}\n\n{history}", 21 | author="Douglas Miranda", 22 | author_email="douglascoding@gmail.com", 23 | url="https://github.com/douglasmiranda/gconfigs", 24 | packages=find_packages(include=["gconfigs"]), 25 | include_package_data=True, 26 | license="MIT license", 27 | zip_safe=False, 28 | keywords="gconfigs configs environment secrets dotenv 12-factor", 29 | classifiers=[ 30 | "Intended Audience :: Developers", 31 | "License :: OSI Approved :: MIT License", 32 | "Natural Language :: English", 33 | "Programming Language :: Python :: 3.6", 34 | "Programming Language :: Python :: 3.7", 35 | ], 36 | ) 37 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from gconfigs.gconfigs import NOTSET 2 | 3 | 4 | class DummyBackend: 5 | """DummyBackend for testing GConfigs 6 | I'm implementing a immutable dict to store configs. 7 | This is because I don't want `GConfigs` to ever change values into the 8 | backend, all the trimming or whatever must be done in `GConfigs` class. 9 | 10 | So if at some point we implement something that changes data in any backend, 11 | it will fail. 12 | """ 13 | 14 | def __init__(self): 15 | # ref: https://www.python.org/dev/peps/pep-0351/ 16 | 17 | class ImmutableDict(dict): 18 | def __hash__(self): 19 | return id(self) 20 | 21 | def _immutable(self, *args, **kws): 22 | raise TypeError("object is immutable") 23 | 24 | __setitem__ = _immutable 25 | __delitem__ = _immutable 26 | clear = _immutable 27 | update = _immutable 28 | setdefault = _immutable 29 | pop = _immutable 30 | popitem = _immutable 31 | 32 | self.data = ImmutableDict( 33 | { 34 | "CONFIG-1": "config-1", 35 | " white space key ": " white space value ", 36 | "empty-value": "", 37 | "space-only-value": " ", 38 | "CONFIG-NONE": None, 39 | "CONFIG-TRUE": True, 40 | "CONFIG-FALSE": False, 41 | "CONFIG-INT": 1, 42 | "CONFIG-FLOAT": 1.1, 43 | "CONFIG-LIST": [1, 1.1, "a"], 44 | "CONFIG-TUPLE": (1, 1.1, "a"), 45 | "CONFIG-DICT": {"a": 1, "b": "b"}, 46 | "CONFIG-TRUE-STRING": "True", 47 | "CONFIG-FALSE-STRING": "False", 48 | "CONFIG-LIST-STRING-JSON-STYLE": '[1, 1.1, "a"]', 49 | "CONFIG-LIST-STRING-JSON-STYLE-BROKEN": '[1, 1.1, "a]', 50 | "CONFIG-DICT-JSON-STYLE": '{"a": 1, "b": "b"}', 51 | } 52 | ) 53 | 54 | def keys(self): 55 | """ 56 | :returns: from iterable `self.data` (an immutable dict) return its keys. 57 | """ 58 | return self.data 59 | 60 | def get(self, key, **kwargs): 61 | """ Return value given a key 62 | :returns: value or KeyError 63 | """ 64 | value = self.data.get(key, NOTSET) 65 | if value is NOTSET: 66 | raise KeyError( 67 | f"Dummy Data '{key}' not set. Check for any " 68 | "misconfiguration or misspelling of the variable name." 69 | ) 70 | 71 | return value 72 | -------------------------------------------------------------------------------- /tests/files/config-files/.env: -------------------------------------------------------------------------------- 1 | [section] 2 | CONFIG-1=config-1 3 | config-2=2 4 | config-key-with-spaces =2 5 | # COMMENTED-CONFIG=1 6 | CONFIG-EMPTY-VALUE= 7 | # Bellow a invalid line, key and value empty 8 | = 9 | config-value-with-spaces= config value with spaces 10 | -------------------------------------------------------------------------------- /tests/files/config-files/.env-empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/douglasmiranda/gconfigs/3cfca5700ee51994420ed87752e93e79afe65c50/tests/files/config-files/.env-empty -------------------------------------------------------------------------------- /tests/files/configs/CONFIG_TEST: -------------------------------------------------------------------------------- 1 | config-test -------------------------------------------------------------------------------- /tests/test_backends.py: -------------------------------------------------------------------------------- 1 | """Tests for `gconfigs.backends` package. 2 | """ 3 | import os 4 | 5 | import pytest 6 | 7 | from gconfigs.backends import LocalEnv, LocalMountFile, DotEnv, RootDirectoryNotFound 8 | 9 | 10 | def test_local_env(): 11 | """Tests for `gconfigs.backends.LocalEnv` 12 | """ 13 | os.environ.update({"GCONFIGS_ENV_TEST": "env-test-1"}) 14 | backend = LocalEnv() 15 | 16 | assert "GCONFIGS_ENV_TEST" in backend.keys() 17 | assert backend.get("GCONFIGS_ENV_TEST") == "env-test-1" 18 | with pytest.raises(KeyError): 19 | backend.get("GCONFIGS_NON-EXISTENT-ENV-KEY") 20 | 21 | 22 | def test_local_mount_file_generic(): 23 | """Tests for `gconfigs.backends.LocalMountFile` with root_dir='./tests/files/configs' 24 | """ 25 | with pytest.raises(RootDirectoryNotFound): 26 | backend = LocalMountFile(root_dir="./NON-EXISTENT-DIR") 27 | assert backend.root_dir 28 | 29 | backend = LocalMountFile(root_dir="./tests/files/configs") 30 | assert "CONFIG_TEST" in backend.keys() 31 | assert backend.get("CONFIG_TEST") == "config-test" 32 | 33 | with pytest.raises(FileNotFoundError): 34 | backend.get("NON-EXISTENT-CONFIG") 35 | 36 | 37 | def test_dotenv(): 38 | backend = DotEnv() 39 | with pytest.raises(Exception, match=r".*you didn't loaded the your dotenv file.*"): 40 | backend.get("CONFIG-1") 41 | 42 | backend.load_file("./tests/files/config-files/.env") 43 | assert "CONFIG-1" in backend.keys() 44 | with pytest.raises(KeyError): 45 | backend.get("NON-EXISTENT-CONFIG-KEY") 46 | 47 | # `.get` must filter break lines 48 | assert "\n" not in backend.get("CONFIG-1") and "\r" not in backend.get( 49 | "CONFIG-1" 50 | ), "Breaklines on values must be removed." 51 | 52 | assert backend.get("CONFIG-1") == "config-1" 53 | assert "config-key-with-spaces" in backend.keys(), "Keys must be stripped." 54 | assert ( 55 | backend.get("config-value-with-spaces") == " config value with spaces" 56 | ), "values should NOT be stripped." 57 | 58 | assert "COMMENTED-CONFIG" not in backend.keys() 59 | assert backend.get("CONFIG-EMPTY-VALUE") == "", "Empty values must be empty string." 60 | 61 | # just seeing if we can iterate over all configs 62 | for key in backend.keys(): 63 | assert not key.startswith(("#", ";", "[")), "Invalid lines must be removed." 64 | assert "=" not in key 65 | # the `CONFIG-EMPTY-VALUE` will be an empty value, so, no good to assert 66 | if key == "CONFIG-EMPTY-VALUE": 67 | continue 68 | 69 | assert backend.get(key) 70 | 71 | # Empty .env file 72 | backend.load_file("./tests/files/config-files/.env-empty") 73 | assert not backend.keys() 74 | 75 | 76 | with pytest.raises(FileNotFoundError): 77 | backend.load_file("./tests/files/config-files/NON-EXISTENT-DOTENV-FILE") 78 | -------------------------------------------------------------------------------- /tests/test_gconfigs_api.py: -------------------------------------------------------------------------------- 1 | """Tests for `gconfigs` package. 2 | - There are some asserts that may seem obvious, that's just because they 3 | act like a restriction to things I think, at the moment, that we may have to 4 | talk before changing the way it's implemented. 5 | """ 6 | 7 | from gconfigs import GConfigs 8 | from . import DummyBackend 9 | import gconfigs 10 | 11 | import pytest 12 | 13 | import json 14 | 15 | 16 | def test_bad_instatiation(): 17 | class MissingGet: 18 | pass 19 | 20 | class MissingKeys: 21 | pass 22 | 23 | with pytest.raises(AttributeError): 24 | GConfigs(backend=MissingGet) 25 | with pytest.raises(AttributeError): 26 | GConfigs(backend=MissingKeys) 27 | 28 | with pytest.raises(TypeError, match=r".*required positional argument: 'backend'.*"): 29 | GConfigs() 30 | 31 | 32 | def test_basics(): 33 | # main endpoints 34 | gconfigs.envs 35 | gconfigs.dotenvs 36 | gconfigs.configs 37 | gconfigs.secrets 38 | # basic expected 39 | configs_ = GConfigs(backend=DummyBackend) 40 | configs_.strip 41 | configs_.object_type_name 42 | configs_.get 43 | configs_.__call__ 44 | # iterator 45 | configs_.iterator 46 | configs_.__next__ 47 | configs_.__iter__ 48 | # fancy xD 49 | configs_.json 50 | configs_.__contains__ 51 | configs_.__len__ 52 | configs_.__repr__ 53 | 54 | assert ( 55 | configs_.object_type_name == "KeyValue" 56 | ), "'object_type_name' default value should be 'KeyValue'" 57 | assert configs_.strip, "'strip' default value should be boolean 'True'" 58 | 59 | configs = GConfigs(backend=DummyBackend, object_type_name="DummyConfig") 60 | assert configs.object_type_name == "DummyConfig" 61 | configs.object_type_name = "DummyConfigChanged" 62 | assert configs.object_type_name == "DummyConfigChanged" 63 | 64 | 65 | def test_get_configs_info(): 66 | configs = GConfigs(backend=DummyBackend) 67 | # if given key is in configs (backend.keys()) 68 | assert "CONFIG-1" in configs 69 | assert "NON-EXISTENT-CONFIG" not in configs 70 | assert len(configs) == len(configs._backend.data) 71 | assert configs("CONFIG-1") == "config-1" 72 | 73 | # Non existent config 74 | with pytest.raises(KeyError): 75 | configs("NON-EXISTENT-CONFIG") 76 | 77 | # Non existent config - return default instead 78 | assert configs("NON-EXISTENT-CONFIG", default="default") == "default" 79 | # default argument can be anything 80 | # achieved by using the `gconfigs.gconfigs.NOTSET` trick 81 | assert configs("NON-EXISTENT-CONFIG", default=None) is None 82 | assert configs("NON-EXISTENT-CONFIG", default=True) is True 83 | assert configs("NON-EXISTENT-CONFIG", default=False) is False 84 | 85 | 86 | def test_get_configs_use_instead(): 87 | configs = GConfigs(backend=DummyBackend) 88 | 89 | # First key doesn't exist, use key/config "CONFIG-1" instead 90 | assert configs("NON-EXISTENT-CONFIG", use_instead="CONFIG-1") == "config-1" 91 | # neither key nor use_instead exist, so use the default 92 | assert configs("NON-EXISTENT-CONFIG", use_instead="NON-EXISTENT-CONFIG-2", default="abc") == "abc" 93 | 94 | # if we don't have a default value; and key and use_instead doesn't exist; 95 | # we get a KeyError about the use_instead key not found. 96 | with pytest.raises(KeyError, match=r".*'NON-EXISTENT-CONFIG-2' not set.*"): 97 | assert configs("NON-EXISTENT-CONFIG", use_instead="NON-EXISTENT-CONFIG-2") 98 | 99 | 100 | def test_get_configs_strip_value_or_not(): 101 | configs = GConfigs(backend=DummyBackend) 102 | assert configs.strip 103 | # strip default: True 104 | assert ( 105 | configs(" white space key ") == "white space value" 106 | and configs("space-only-value") == "" 107 | ), "Returning value should be stripped." 108 | # override default 109 | assert ( 110 | configs(" white space key ", strip=False) == " white space value " 111 | and configs("space-only-value", strip=False) == " " 112 | ), "Returning value should ALLOW blank spaces." 113 | 114 | # no strip 115 | configs2 = GConfigs(backend=DummyBackend, strip=False) 116 | assert configs2.strip is False 117 | # strip default: False 118 | assert ( 119 | configs2(" white space key ") == " white space value " 120 | and configs2("space-only-value") == " " 121 | ), "Returning value should ALLOW blank spaces." 122 | # override default 123 | assert ( 124 | configs2(" white space key ", strip=True) == "white space value" 125 | and configs2("space-only-value", strip=True) == "" 126 | ), "Returning value should be stripped." 127 | configs2.strip = True 128 | assert configs2.strip 129 | 130 | 131 | def test_get_typed_configs(): 132 | """Check if gConfigs deal with typed configs 133 | Most backends will probably return strings as value of a config, but 134 | if someone cast the value it's nice to know if gConfigs is ok with this. 135 | """ 136 | configs = GConfigs(backend=DummyBackend) 137 | assert configs("CONFIG-1") == "config-1" 138 | assert configs("CONFIG-NONE") is None 139 | assert configs("CONFIG-TRUE") is True 140 | assert configs("CONFIG-FALSE") is False 141 | assert configs("CONFIG-INT") == 1 142 | assert configs("CONFIG-FLOAT") == 1.1 143 | assert configs("CONFIG-LIST") == [1, 1.1, "a"] 144 | assert configs("CONFIG-TUPLE") == (1, 1.1, "a") 145 | assert configs("CONFIG-DICT") == {"a": 1, "b": "b"} 146 | 147 | 148 | def test_get_and_cast_value(): 149 | configs = GConfigs(backend=DummyBackend) 150 | # BOOLEAN 151 | assert configs.as_bool("CONFIG-TRUE") # when backend return a bool already 152 | assert configs.as_bool("CONFIG-TRUE-STRING") 153 | assert isinstance(configs.as_bool("CONFIG-TRUE-STRING"), bool) 154 | assert not configs.as_bool("CONFIG-FALSE-STRING") 155 | assert isinstance(configs.as_bool("CONFIG-FALSE-STRING"), bool) 156 | assert configs.as_bool("NON-EXISTENT-CONFIG", default=True) 157 | assert isinstance(configs.as_bool("NON-EXISTENT-CONFIG", default=True), bool) 158 | with pytest.raises(ValueError, match=r".*Could not cast the value.*"): 159 | configs.as_bool("CONFIG-NONE") 160 | 161 | # LIST 162 | assert configs.as_list("CONFIG-LIST") == [1, 1.1, "a"] 163 | assert configs.as_list("CONFIG-TUPLE") == [1, 1.1, "a"] 164 | assert configs.as_list("CONFIG-LIST-STRING-JSON-STYLE") == [1, 1.1, "a"] 165 | with pytest.raises(ValueError, match=r".*Could not cast the value.*"): 166 | configs.as_list("CONFIG-NONE") 167 | 168 | # DICT 169 | assert configs.as_dict("CONFIG-DICT") == {"a": 1, "b": "b"} 170 | assert configs.as_dict("CONFIG-DICT-JSON-STYLE") == {"a": 1, "b": "b"} 171 | with pytest.raises(ValueError, match=r".*Could not cast the value.*"): 172 | configs.as_dict("CONFIG-NONE") 173 | 174 | # trigger exception on GConfigs._cast() 175 | with pytest.raises(ValueError, match=r".*must be a valid json value.*"): 176 | configs.as_list("CONFIG-LIST-STRING-JSON-STYLE-BROKEN") 177 | 178 | 179 | def test_backend_with_load_file_method(): 180 | """ Testing backend with ``load_file(filepath: str)`` method 181 | I just need a backend with the ``load_file(filepath: str)`` 182 | and test if ```load_file(filepath: str)`` will also be available in a 183 | instance of ``GConfigs()``. 184 | 185 | The actual test of how ``load_file`` works it's 186 | """ 187 | 188 | class DummyBackendLoadFile(DummyBackend): 189 | def load_file(self, filepath): 190 | pass 191 | 192 | configs = GConfigs(backend=DummyBackendLoadFile) 193 | assert configs.load_file 194 | configs.load_file("foo") 195 | 196 | # Instances of ``Gconfigs``, using backends with 197 | # no ``load_file(filepath: str)`` should not have a ``load_file`` itself. 198 | with pytest.raises( 199 | AttributeError, match=r".*'GConfigs' object has no attribute 'load_file'.*" 200 | ): 201 | GConfigs(backend=DummyBackend).load_file 202 | 203 | 204 | def test_iterator(): 205 | configs = GConfigs(backend=DummyBackend) 206 | # iterate with forloop 207 | for config in configs: 208 | # Make sure the object is a namedtuple 209 | assert ( 210 | issubclass(config.__class__, tuple) and config._fields 211 | ), "Looks like the iterator is not returning a namedtuple anymore." 212 | assert config 213 | assert config.key 214 | # guarantee the namedtuple is being properly created 215 | assert ( 216 | configs(config.key) == config.value 217 | ), "Looks like the namedtuple is not being properly created." 218 | 219 | # after an iterate in forloops it should have nothing left for next() 220 | with pytest.raises(StopIteration): 221 | next(configs) 222 | 223 | # but it's possible to iterator as many as you can with `self.iterator()` 224 | iter_configs = configs.iterator() 225 | for config in iter_configs: 226 | assert config 227 | 228 | 229 | def test_json(): 230 | configs = GConfigs(backend=DummyBackend) 231 | json_ = configs.json() 232 | assert json_ 233 | # let's turn the json_ into a dict so we can see if it preserved all the configs 234 | configs_data = json.loads(json_) 235 | assert all(key in configs._backend.data for key in configs_data) 236 | 237 | assert next( 238 | configs 239 | ), "If `configs` is not iterable after `configs.json()` it's because it's not using the iterator properly." 240 | --------------------------------------------------------------------------------