Source code for dynaconf.cli

import io
import os
import sys
import toml
import click
import pprint
import importlib
import webbrowser
from pathlib import Path
from dynaconf import default_settings
from dynaconf import constants
from dynaconf import LazySettings
from dynaconf.validator import Validator, ValidationError
from dynaconf.utils.parse_conf import parse_conf_data
from dotenv import cli as dotenv_cli
from contextlib import suppress

CWD = Path.cwd()
ENVS = ['default', 'development', 'staging', 'testing', 'production', 'global']
EXTS = ['ini', 'toml', 'yaml', 'json', 'py', 'env']
WRITERS = ['ini', 'toml', 'yaml', 'json', 'py', 'redis', 'vault', 'env']

ENC = default_settings.ENCODING_FOR_DYNACONF


[docs]def set_settings(instance=None): """Pick correct settings instance and set it to a global variable.""" global settings settings = None if instance: settings = import_settings(instance) elif 'INSTANCE_FOR_DYNACONF' in os.environ: settings = import_settings(os.environ['INSTANCE_FOR_DYNACONF']) elif 'FLASK_APP' in os.environ: # pragma: no cover with suppress(ImportError, click.UsageError): from flask.cli import ScriptInfo flask_app = ScriptInfo().load_app() settings = flask_app.config click.echo(click.style( 'Flask app detected', fg='white', bg='black')) elif 'DJANGO_SETTINGS_MODULE' in os.environ: # pragma: no cover sys.path.insert(0, os.path.abspath('.')) with suppress(Exception): import dynaconf.contrib.django_dynaconf # noqa from django.conf import settings as django_settings django_settings.configure() settings = django_settings click.echo(click.style( 'Django app detected', fg='white', bg='black')) if settings is None: settings = LazySettings()
[docs]def import_settings(dotted_path): """Import settings instance from python dotted path. Last item in dotted path must be settings instace. Example: import_settings('path.to.settings') """ if '.' in dotted_path: module, name = dotted_path.rsplit('.', 1) else: raise click.UsageError( "invalid path to settings instance: {}".format(dotted_path)) try: module = importlib.import_module(module) except ImportError as e: raise click.UsageError(e) try: return getattr(module, name) except AttributeError as e: raise click.UsageError(e)
[docs]def split_vars(_vars): """Splits values like foo=bar=zaz in {'foo': 'bar=zaz'}""" return { k.upper().strip(): parse_conf_data(v.strip(), tomlfy=True) for k, _, v in [item.partition('=') for item in _vars] } if _vars else {}
[docs]def read_file_in_root_directory(*names, **kwargs): """Read a file.""" return io.open( os.path.join(os.path.dirname(__file__), *names), encoding=kwargs.get('encoding', 'utf-8') ).read().strip()
[docs]def open_docs(ctx, param, value): # pragma: no cover if not value or ctx.resilient_parsing: return url = 'http://dynaconf.readthedocs.io/' webbrowser.open(url, new=2) click.echo("{} opened in browser".format(url)) ctx.exit()
[docs]def show_banner(ctx, param, value): """Shows dynaconf awesome banner""" if not value or ctx.resilient_parsing: return set_settings() click.echo(settings.dynaconf_banner) click.echo('Learn more at: http://github.com/rochacbruno/dynaconf') ctx.exit()
@click.group() @click.option('--version', is_flag=True, callback=print_version, expose_value=False, is_eager=True, help="Show dynaconf version") @click.option('--docs', is_flag=True, callback=open_docs, expose_value=False, is_eager=True, help="Open documentation in browser") @click.option('--banner', is_flag=True, callback=show_banner, expose_value=False, is_eager=True, help="Show awesome banner") @click.option('--instance', '-i', default=None, help="Custom instance of LazySettings") def main(instance): """Dynaconf - Command Line Interface\n Documentation: http://dynaconf.readthedocs.io/ """ set_settings(instance) @main.command() @click.option('--format', 'fileformat', '-f', default='toml', type=click.Choice(EXTS)) @click.option('--path', '-p', default=CWD, help='defaults to current directory') @click.option('--env', '-e', default=None, help='Sets the working env in `.env` file') @click.option('--vars', '_vars', '-v', multiple=True, default=None, help=( 'extra values to write to settings file ' 'file e.g: `dynaconf init -v NAME=foo -v X=2' )) @click.option('--secrets', '_secrets', '-s', multiple=True, default=None, help=( 'secret key values to be written in .secrets ' 'e.g: `dynaconf init -s TOKEN=kdslmflds' )) @click.option('--wg/--no-wg', default=True) @click.option('-y', default=False, is_flag=True) def init(fileformat, path, env, _vars, _secrets, wg, y): """Inits a dynaconf project By default it creates a settings.toml and a .secrets.toml for [default|development|staging|testing|production|global] envs. The format of the files can be changed passing --format=yaml|json|ini|py. This command must run on the project's root folder or you must pass --path=/myproject/root/folder. If you want to have a .env created with the ENV defined there e.g: `ENV_FOR_DYNACONF=production` just pass --env=production and then .env will also be created and the env defined to production. """ click.echo('Cofiguring your Dynaconf environment') env = env or settings.current_env.lower() loader = importlib.import_module( "dynaconf.loaders.{}_loader".format(fileformat) ) # Turn foo=bar=zaz in {'foo': 'bar=zaz'} env_data = split_vars(_vars) _secrets = split_vars(_secrets) # create placeholder data for every env settings_data = {k: {'value': 'value for {}'.format(k)} for k in ENVS} secrets_data = {k: {'secret': 'secret for {}'.format(k)} for k in ENVS} if env_data: settings_data[env] = env_data settings_data['default'] = {k: 'default' for k in env_data} if _secrets: secrets_data[env] = _secrets secrets_data['default'] = {k: 'default' for k in _secrets} path = Path(path) if str(path).endswith(constants.ALL_EXTENSIONS + ('py',)): # pragma: no cover # noqa settings_path = path secrets_path = path.parent / '.secrets.{}'.format(fileformat) dotenv_path = path.parent / '.env' gitignore_path = path.parent / '.gitignore' else: if fileformat == 'env': if str(path) in ('.env', './.env'): # pragma: no cover settings_path = path elif str(path).endswith('/.env'): # pragma: no cover settings_path = path elif str(path).endswith('.env'): # pragma: no cover settings_path = path.parent / '.env' else: settings_path = path / '.env' Path.touch(settings_path) secrets_path = None else: settings_path = path / 'settings.{}'.format(fileformat) secrets_path = path / '.secrets.{}'.format(fileformat) dotenv_path = path / '.env' gitignore_path = path / '.gitignore' if fileformat in ['py', 'env']: # for Python and .env files writes a single env settings_data = settings_data[env] secrets_data = secrets_data[env] if not y and settings_path and settings_path.exists(): # pragma: no cover click.confirm( '{} exists do you want to overwrite it?'.format(settings_path), abort=True ) if not y and secrets_path and secrets_path.exists(): # pragma: no cover click.confirm( '{} exists do you want to overwrite it?'.format(secrets_path), abort=True ) if settings_path and settings_data: loader.write(settings_path, settings_data, merge=True) if secrets_path and secrets_data: loader.write(secrets_path, secrets_data, merge=True) # write .env file # if env not in ['default', 'development']: # pragma: no cover if not dotenv_path.exists(): # pragma: no cover Path.touch(dotenv_path) dotenv_cli.set_key(str(dotenv_path), 'ENV_FOR_DYNACONF', env.upper()) else: # pragma: no cover click.echo( '.env already exists please set ENV_FOR_DYNACONF={}'.format( env.upper() ) ) if wg: # write .gitignore ignore_line = ".secrets.*" comment = "\n# Ignore dynaconf secret files\n" if not gitignore_path.exists(): with io.open(str(gitignore_path), 'w', encoding=ENC) as f: f.writelines([comment, ignore_line, '\n']) else: existing = ignore_line in io.open( str(gitignore_path), encoding=ENC ).read() if not existing: # pragma: no cover with io.open(str(gitignore_path), 'a+', encoding=ENC) as f: f.writelines( [comment, ignore_line, '\n'] ) @main.command(name='list') @click.option('--env', '-e', default=None, help='Filters the env to get the values') @click.option('--key', '-k', default=None, help='Filters a single key') @click.option('--more', '-m', default=None, help='Pagination more|less style', is_flag=True) @click.option('--loader', '-l', default=None, help='a loader identifier to filter e.g: toml|yaml') def _list(env, key, more, loader): """Lists all defined config values""" if env: env = env.strip() if key: key = key.strip() if loader: loader = loader.strip() if env: settings.setenv(env) cur_env = settings.current_env.lower() click.echo( click.style( 'Working in %s environment ' % cur_env, bold=True, bg='blue', fg='white' ) ) if not loader: data = settings.store else: identifier = '{}_{}'.format(loader, cur_env) data = settings._loaded_by_loaders.get(identifier, {}) data = data or settings._loaded_by_loaders.get(loader, {}) # remove to avoid displaying twice data.pop('SETTINGS_MODULE', None) def color(_k): if _k in dir(default_settings): return 'blue' return 'green' if not key: datalines = '\n'.join( '%s: %s' % (click.style(k, bg=color(k), fg='white'), pprint.pformat(v)) for k, v in data.items() ) (click.echo_via_pager if more else click.echo)(datalines) else: key = key.upper() value = data.get(key) if not value: click.echo(click.style('Key not found', bg='red', fg='white')) return click.echo( '%s: %s' % ( click.style(key.upper(), bg=color(key), fg='white'), pprint.pformat(value) ) ) if env: settings.setenv() @main.command() @click.argument('to', required=True, type=click.Choice(WRITERS)) @click.option('--vars', '_vars', '-v', multiple=True, default=None, help=( 'key values to be written ' 'e.g: `dynaconf write toml -e NAME=foo -e X=2' )) @click.option('--secrets', '_secrets', '-s', multiple=True, default=None, help=( 'secret key values to be written in .secrets ' 'e.g: `dynaconf write toml -s TOKEN=kdslmflds -s X=2' )) @click.option('--path', '-p', default=CWD, help='defaults to current directory/settings.{ext}') @click.option( '--env', '-e', default='default', help=( 'env to write to defaults to DEVELOPMENT for files ' 'for external sources like Redis and Vault ' 'it will be DYNACONF or the value set in ' '$GLOBAL_ENV_FOR_DYNACONF' ) ) @click.option('-y', default=False, is_flag=True) def write(to, _vars, _secrets, path, env, y): """Writes data to specific source""" _vars = split_vars(_vars) _secrets = split_vars(_secrets) loader = importlib.import_module("dynaconf.loaders.{}_loader".format(to)) if to in EXTS: # Lets write to a file path = Path(path) if str(path).endswith(constants.ALL_EXTENSIONS + ('py',)): settings_path = path secrets_path = path.parent / '.secrets.{}'.format(to) else: if to == 'env': if str(path) in ('.env', './.env'): # pragma: no cover settings_path = path elif str(path).endswith('/.env'): settings_path = path elif str(path).endswith('.env'): settings_path = path.parent / '.env' else: settings_path = path / '.env' Path.touch(settings_path) secrets_path = None _vars.update(_secrets) else: settings_path = path / 'settings.{}'.format(to) secrets_path = path / '.secrets.{}'.format(to) if _vars and not y and settings_path and settings_path.exists(): # pragma: no cover # noqa click.confirm( '{} exists do you want to overwrite it?'.format(settings_path), abort=True ) if _secrets and not y and secrets_path and secrets_path.exists(): # pragma: no cover # noqa click.confirm( '{} exists do you want to overwrite it?'.format(secrets_path), abort=True ) if to not in ['py', 'env']: if _vars: _vars = {env: _vars} if _secrets: _secrets = {env: _secrets} if _vars and settings_path: loader.write(settings_path, _vars, merge=True) click.echo('Data successful written to {}'.format(settings_path)) if _secrets and secrets_path: loader.write(secrets_path, _secrets, merge=True) click.echo('Data successful written to {}'.format(secrets_path)) else: # pragma: no cover # lets write to external source with settings.using_env(env): # make sure we're in the correct environment loader.write(settings, _vars, **_secrets) click.echo('Data successful written to {}'.format(to)) @main.command() @click.option('--path', '-p', default=CWD, help='defaults to current directory') def validate(path): # pragma: no cover """Validates Dynaconf settings based on rules defined in dynaconf_validators.toml""" # reads the 'dynaconf_validators.toml' from path # for each section register the validator for specific env # call validate path = Path(path) if not str(path).endswith('.toml'): path = path / "dynaconf_validators.toml" if not path.exists(): # pragma: no cover # noqa click.echo(click.style( "{} not found".format(path), fg="white", bg="red" )) sys.exit(1) validation_data = toml.load(open(str(path))) success = True for env, name_data in validation_data.items(): for name, data in name_data.items(): if not isinstance(data, dict): # pragma: no cover click.echo(click.style( "Invalid rule for parameter '{}'".format(name), fg="white", bg="yellow" )) else: data.setdefault('env', env) click.echo(click.style( "Validating '{}' with '{}'".format(name, data), fg="white", bg="blue" )) try: Validator(name, **data).validate(settings) except ValidationError as e: click.echo(click.style( "Error: {}".format(e), fg="white", bg="red" )) success = False if success: click.echo(click.style( "Validation success!", fg="white", bg="green" ))