Source code for dynaconf.cli

import importlib
import io
import os
import pprint
import sys
import webbrowser
from contextlib import suppress
from pathlib import Path

import click
import toml
from dotenv import cli as dotenv_cli

from dynaconf import constants
from dynaconf import default_settings
from dynaconf import LazySettings
from dynaconf import loaders
from dynaconf.loaders.py_loader import get_module
from dynaconf.utils import upperfy
from dynaconf.utils.files import read_file
from dynaconf.utils.parse_conf import parse_conf_data
from dynaconf.validator import ValidationError
from dynaconf.validator import Validator


CWD = Path.cwd()
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="bright_black" ) ) elif "DJANGO_SETTINGS_MODULE" in os.environ: # pragma: no cover sys.path.insert(0, os.path.abspath(os.getcwd())) try: # Django extension v2 from django.conf import settings settings.DYNACONF.configure() except (ImportError, AttributeError): # Backwards compatible with old django extension (pre 2.0.0) import dynaconf.contrib.django_dynaconf # noqa from django.conf import settings as django_settings django_settings.configure() settings = django_settings if settings is not None: click.echo( click.style( "Django app detected", fg="white", bg="bright_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 ( { upperfy(k.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 on root dir.""" return read_file( os.path.join(os.path.dirname(__file__), *names), encoding=kwargs.get("encoding", "utf-8"), )
[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) @click.option("--django", default=os.environ.get("DJANGO_SETTINGS_MODULE")) def init(fileformat, path, env, _vars, _secrets, wg, y, django): """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("Configuring 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 = {} secrets_data = {} 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"]) if django: # pragma: no cover dj_module, loaded_from = get_module({}, django) dj_filename = dj_module.__file__ if Path(dj_filename).exists(): click.confirm( "{} is found do you want to add dynaconf?".format(dj_filename), abort=True, ) with open(dj_filename, "a") as dj_file: dj_file.write(constants.DJANGO_PATCH) else: click.echo("Django settings file not written.") @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", ) @click.option( "--all", "_all", "-a", default=False, is_flag=True, help="show dynaconf internal settings?", ) @click.option( "--output", "-o", type=click.Path(writable=True, dir_okay=False), default=None, help="Filepath to write the listed values as json", ) @click.option( "--output-flat", "flat", is_flag=True, default=False, help="Output file is flat (do not include [env] name)", ) def _list(env, key, more, loader, _all=False, output=None, flat=False): """Lists all user defined config values and if `--all` is passed it also shows dynaconf internal variables. """ 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="bright_black", ) ) if not loader: data = settings.as_dict(env=env, internal=_all) 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) if output: loaders.write(output, data, env=not flat and cur_env) else: key = upperfy(key) 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(upperfy(key), bg=color(key), fg="white"), pprint.pformat(value), ) ) if output: loaders.write( output, {upperfy(key): value}, env=not flat and cur_env ) 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 " "$ENVVAR_PREFIX_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")) else: click.echo(click.style("Validation error!", fg="white", bg="red")) sys.exit(1) if __name__ == "__main__": # pragma: no cover main()