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 print_version(ctx, param, value):
if not value or ctx.resilient_parsing:
return
click.echo(read_file_in_root_directory("VERSION"))
ctx.exit()
[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()