Source code for cli2.configuration
"""
12-factor interactive self-building lazy configuration.
The developer story we are after:
- there's nothing to do, just expect ``cli2.cfg['YOUR_ENV_VAR']`` to work one
way or another
- when there's a minute to be nice to the user, add some help that will be
displayed to them: ``cli2.cfg.questions['YOUR_ENV_VAR'] = 'this is a help
text that will be display to the user when we prompt them for YOUR_ENV_VAR'``
- you can also set ``cli2.cfg.defaults['YOUR_ENV_VAR']`` if you prefer a
default value to an interactive prompt
This is the user story we are after:
- user runs your cli2 command right after install without any configuration
- the user is prompted for a variable
- the variable is saved in their ~/.profile in a new export line
- the user runs a command again in the same shell: we should find the variable
in ~/.profile so he doesn't have to start a new shell for his new
configuration to work
Of course, if the environment variable is already present in the environment
then this basically returns it from ``os.environ``.
"""
import functools
import os
import re
import shlex
import textwrap
from pathlib import Path
[docs]
class Configuration(dict):
"""
Configuration object.
Wraps around environment variable and can question the user for missing
variables and save them in his shell profile.
.. py:attribute:: questions
A dict of ``ENV_VAR=question_string``, if an env var is missing from
configuration then question_string will be used as text to prompt the
user.
.. py:attribute:: defaults
A dict of ``ENV_VAR='default_value'``, if an env var is missing from
configuration then the default value will be returned instead of
displaying an interactive prompt.
.. py:attribute:: profile_path
Path to the shell profile to save/read variables, defaults to
~/.profile which should work in many shells.
You can also just work with the module level ("singleton") instance, have
scripts like:
.. code-block:: python
import cli2
cli = cli2.Group()
cli2.cfg['API_URL'] = 'What is your API URL?'
@cli.cmd
def foo():
api_url = cli2.cfg['API_URL']
# when there's no question, it'll use the var name as prompt
api_url = cli2.cfg['USERNAME']
"""
def __init__(self, profile_path=None, defaults=None, **questions):
self.questions = questions
self.defaults = defaults or dict()
self.profile_path = Path(
profile_path or os.getenv('HOME') + '/.profile'
)
self._profile_script = None
self._profile_variables = dict()
self.environ = os.environ.copy()
def __getitem__(self, key):
"""
If the key is not in self, call :py:meth:`configure`.
:param key: Environment variable name
"""
if key not in self:
self[key] = self.configure(key)
return super().__getitem__(key)
@property
def profile_script(self):
"""
Cached :py:attr:`profile_path` reader.
"""
if self._profile_script:
return self._profile_script
if self.profile_path.exists():
with self.profile_path.open('r') as f:
self._profile_script = f.read()
else:
self.profile_path.touch()
self._profile_script = ''
return self._profile_script
@functools.cached_property
def profile_variables(self):
"""
Cached environment variable parsing from :py:attr:`profile_path`.
"""
if self._profile_variables:
return self._profile_variables
for line in self.profile_script.split('\n'):
if not line.startswith('export '):
continue
name, value = re.findall('export ([^=]*)=(.*)', line)[0]
value = shlex.split(value)[0]
self._profile_variables[name] = value
return self._profile_variables
def print(self, *args, **kwargs):
print(*args, **kwargs)
[docs]
def delete(self, key, reason=None):
"""
Delete a variable from everywhere, useful if an api key expired.
:param key: Env var name to delete
:param reason: Reason to print to the user
"""
with self.profile_path.open('r') as f:
lines = f.read().split('\n')
contents = [
line
for line in lines
if not line.startswith(f'export {key}=')
]
if len(contents) != len(lines):
if reason:
print(reason)
print(f'Removing {key} configuration')
new_script = '\n'.join(contents)
with self.profile_path.open('w') as f:
f.write(new_script)
self._profile_script = new_script
self._profile_variables.pop(key, None)
self.pop(key, None)
self.environ.pop(key, None)
cfg = Configuration()