commit
8282899fac
@ -0,0 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dynaconf.base import LazySettings # noqa
|
||||
from dynaconf.constants import DEFAULT_SETTINGS_FILES
|
||||
from dynaconf.contrib import DjangoDynaconf # noqa
|
||||
from dynaconf.contrib import FlaskDynaconf # noqa
|
||||
from dynaconf.validator import ValidationError # noqa
|
||||
from dynaconf.validator import Validator # noqa
|
||||
|
||||
settings = LazySettings(
|
||||
# This global `settings` is deprecated from v3.0.0+
|
||||
# kept here for backwards compatibility
|
||||
# To Be Removed in 4.0.x
|
||||
warn_dynaconf_global_settings=True,
|
||||
environments=True,
|
||||
lowercase_read=False,
|
||||
load_dotenv=True,
|
||||
default_settings_paths=DEFAULT_SETTINGS_FILES,
|
||||
)
|
||||
|
||||
# This is the new recommended base class alias
|
||||
Dynaconf = LazySettings # noqa
|
||||
|
||||
__all__ = [
|
||||
"Dynaconf",
|
||||
"LazySettings",
|
||||
"Validator",
|
||||
"FlaskDynaconf",
|
||||
"ValidationError",
|
||||
"DjangoDynaconf",
|
||||
]
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,773 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
import pprint
|
||||
import sys
|
||||
import warnings
|
||||
import webbrowser
|
||||
from contextlib import suppress
|
||||
from pathlib import Path
|
||||
|
||||
from dynaconf import constants
|
||||
from dynaconf import default_settings
|
||||
from dynaconf import LazySettings
|
||||
from dynaconf import loaders
|
||||
from dynaconf import settings as legacy_settings
|
||||
from dynaconf.loaders.py_loader import get_module
|
||||
from dynaconf.utils import upperfy
|
||||
from dynaconf.utils.files import read_file
|
||||
from dynaconf.utils.functional import empty
|
||||
from dynaconf.utils.parse_conf import parse_conf_data
|
||||
from dynaconf.utils.parse_conf import unparse_conf_data
|
||||
from dynaconf.validator import ValidationError
|
||||
from dynaconf.validator import Validator
|
||||
from dynaconf.vendor import click
|
||||
from dynaconf.vendor import toml
|
||||
from dynaconf.vendor import tomllib
|
||||
|
||||
os.environ["PYTHONIOENCODING"] = "utf-8"
|
||||
|
||||
CWD = None
|
||||
try:
|
||||
CWD = Path.cwd()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
EXTS = ["ini", "toml", "yaml", "json", "py", "env"]
|
||||
WRITERS = ["ini", "toml", "yaml", "json", "py", "redis", "vault", "env"]
|
||||
|
||||
ENC = default_settings.ENCODING_FOR_DYNACONF
|
||||
|
||||
|
||||
def set_settings(ctx, instance=None):
|
||||
"""Pick correct settings instance and set it to a global variable."""
|
||||
|
||||
global settings
|
||||
|
||||
settings = None
|
||||
|
||||
_echo_enabled = ctx.invoked_subcommand not in ["get", None]
|
||||
|
||||
if instance is not None:
|
||||
if ctx.invoked_subcommand in ["init"]:
|
||||
raise click.UsageError(
|
||||
"-i/--instance option is not allowed for `init` command"
|
||||
)
|
||||
sys.path.insert(0, ".")
|
||||
settings = import_settings(instance)
|
||||
elif "FLASK_APP" in os.environ: # pragma: no cover
|
||||
with suppress(ImportError, click.UsageError):
|
||||
from flask.cli import ScriptInfo # noqa
|
||||
from dynaconf import FlaskDynaconf
|
||||
|
||||
flask_app = ScriptInfo().load_app()
|
||||
settings = FlaskDynaconf(flask_app, **flask_app.config).settings
|
||||
_echo_enabled and 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 # noqa
|
||||
|
||||
settings.DYNACONF.configure()
|
||||
except AttributeError:
|
||||
settings = LazySettings()
|
||||
|
||||
if settings is not None:
|
||||
_echo_enabled and click.echo(
|
||||
click.style(
|
||||
"Django app detected", fg="white", bg="bright_black"
|
||||
)
|
||||
)
|
||||
|
||||
if settings is None:
|
||||
|
||||
if instance is None and "--help" not in click.get_os_args():
|
||||
if ctx.invoked_subcommand and ctx.invoked_subcommand not in [
|
||||
"init",
|
||||
]:
|
||||
warnings.warn(
|
||||
"Starting on 3.x the param --instance/-i is now required. "
|
||||
"try passing it `dynaconf -i path.to.settings <cmd>` "
|
||||
"Example `dynaconf -i config.settings list` "
|
||||
)
|
||||
settings = legacy_settings
|
||||
else:
|
||||
settings = LazySettings(create_new_settings=True)
|
||||
else:
|
||||
settings = LazySettings()
|
||||
|
||||
|
||||
def import_settings(dotted_path):
|
||||
"""Import settings instance from python dotted path.
|
||||
|
||||
Last item in dotted path must be settings instance.
|
||||
|
||||
Example: import_settings('path.to.settings')
|
||||
"""
|
||||
if "." in dotted_path:
|
||||
module, name = dotted_path.rsplit(".", 1)
|
||||
else:
|
||||
raise click.UsageError(
|
||||
f"invalid path to settings instance: {dotted_path}"
|
||||
)
|
||||
try:
|
||||
module = importlib.import_module(module)
|
||||
except ImportError as e:
|
||||
raise click.UsageError(e)
|
||||
except FileNotFoundError:
|
||||
return
|
||||
try:
|
||||
return getattr(module, name)
|
||||
except AttributeError as e:
|
||||
raise click.UsageError(e)
|
||||
|
||||
|
||||
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, box_settings=settings
|
||||
)
|
||||
for k, _, v in [item.partition("=") for item in _vars]
|
||||
}
|
||||
if _vars
|
||||
else {}
|
||||
)
|
||||
|
||||
|
||||
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"),
|
||||
)
|
||||
|
||||
|
||||
def print_version(ctx, param, value):
|
||||
if not value or ctx.resilient_parsing:
|
||||
return
|
||||
click.echo(read_file_in_root_directory("VERSION"))
|
||||
ctx.exit()
|
||||
|
||||
|
||||
def open_docs(ctx, param, value): # pragma: no cover
|
||||
if not value or ctx.resilient_parsing:
|
||||
return
|
||||
url = "https://dynaconf.com/"
|
||||
webbrowser.open(url, new=2)
|
||||
click.echo(f"{url} opened in browser")
|
||||
ctx.exit()
|
||||
|
||||
|
||||
def show_banner(ctx, param, value):
|
||||
"""Shows dynaconf awesome banner"""
|
||||
if not value or ctx.resilient_parsing:
|
||||
return
|
||||
set_settings(ctx)
|
||||
click.echo(settings.dynaconf_banner)
|
||||
click.echo("Learn more at: http://github.com/dynaconf/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,
|
||||
envvar="INSTANCE_FOR_DYNACONF",
|
||||
help="Custom instance of LazySettings",
|
||||
)
|
||||
@click.pass_context
|
||||
def main(ctx, instance):
|
||||
"""Dynaconf - Command Line Interface\n
|
||||
Documentation: https://dynaconf.com/
|
||||
"""
|
||||
set_settings(ctx, 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="deprecated command (kept for compatibility but unused)",
|
||||
)
|
||||
@click.option(
|
||||
"--vars",
|
||||
"_vars",
|
||||
"-v",
|
||||
multiple=True,
|
||||
default=None,
|
||||
help=(
|
||||
"extra values to write to settings 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"))
|
||||
@click.pass_context
|
||||
def init(ctx, 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.
|
||||
|
||||
The --env/-e is deprecated (kept for compatibility but unused)
|
||||
"""
|
||||
click.echo("⚙️ Configuring your Dynaconf environment")
|
||||
click.echo("-" * 42)
|
||||
if "FLASK_APP" in os.environ: # pragma: no cover
|
||||
click.echo(
|
||||
"⚠️ Flask detected, you can't use `dynaconf init` "
|
||||
"on a flask project, instead go to dynaconf.com/flask/ "
|
||||
"for more information.\n"
|
||||
"Or add the following to your app.py\n"
|
||||
"\n"
|
||||
"from dynaconf import FlaskDynaconf\n"
|
||||
"app = Flask(__name__)\n"
|
||||
"FlaskDynaconf(app)\n"
|
||||
)
|
||||
exit(1)
|
||||
|
||||
path = Path(path)
|
||||
|
||||
if env is not None:
|
||||
click.secho(
|
||||
"⚠️ The --env/-e option is deprecated (kept for\n"
|
||||
" compatibility but unused)\n",
|
||||
fg="red",
|
||||
bold=True,
|
||||
# stderr=True,
|
||||
)
|
||||
|
||||
if settings.get("create_new_settings") is True:
|
||||
filename = Path("config.py")
|
||||
if not filename.exists():
|
||||
with open(filename, "w") as new_settings:
|
||||
new_settings.write(
|
||||
constants.INSTANCE_TEMPLATE.format(
|
||||
settings_files=[
|
||||
f"settings.{fileformat}",
|
||||
f".secrets.{fileformat}",
|
||||
]
|
||||
)
|
||||
)
|
||||
click.echo(
|
||||
"🐍 The file `config.py` was generated.\n"
|
||||
" on your code now use `from config import settings`.\n"
|
||||
" (you must have `config` importable in your PYTHONPATH).\n"
|
||||
)
|
||||
else:
|
||||
click.echo(
|
||||
f"⁉️ You already have a {filename} so it is not going to be\n"
|
||||
" generated for you, you will need to create your own \n"
|
||||
" settings instance e.g: config.py \n"
|
||||
" from dynaconf import Dynaconf \n"
|
||||
" settings = Dynaconf(**options)\n"
|
||||
)
|
||||
sys.path.append(str(path))
|
||||
set_settings(ctx, "config.settings")
|
||||
|
||||
env = settings.current_env.lower()
|
||||
|
||||
loader = importlib.import_module(f"dynaconf.loaders.{fileformat}_loader")
|
||||
# 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: "a default value" for k in env_data}
|
||||
if _secrets:
|
||||
secrets_data[env] = _secrets
|
||||
secrets_data["default"] = {k: "a default value" for k in _secrets}
|
||||
|
||||
if str(path).endswith(
|
||||
constants.ALL_EXTENSIONS + ("py",)
|
||||
): # pragma: no cover # noqa
|
||||
settings_path = path
|
||||
secrets_path = path.parent / f".secrets.{fileformat}"
|
||||
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 / f"settings.{fileformat}"
|
||||
secrets_path = path / f".secrets.{fileformat}"
|
||||
gitignore_path = path / ".gitignore"
|
||||
|
||||
if fileformat in ["py", "env"] or env == "main":
|
||||
# for Main env, Python and .env formats writes a single env
|
||||
settings_data = settings_data.get(env, {})
|
||||
secrets_data = secrets_data.get(env, {})
|
||||
|
||||
if not y and settings_path and settings_path.exists(): # pragma: no cover
|
||||
click.confirm(
|
||||
f"⁉ {settings_path} exists do you want to overwrite it?",
|
||||
abort=True,
|
||||
)
|
||||
|
||||
if not y and secrets_path and secrets_path.exists(): # pragma: no cover
|
||||
click.confirm(
|
||||
f"⁉ {secrets_path} exists do you want to overwrite it?",
|
||||
abort=True,
|
||||
)
|
||||
|
||||
if settings_path:
|
||||
loader.write(settings_path, settings_data, merge=True)
|
||||
click.echo(
|
||||
f"🎛️ {settings_path.name} created to hold your settings.\n"
|
||||
)
|
||||
|
||||
if secrets_path:
|
||||
loader.write(secrets_path, secrets_data, merge=True)
|
||||
click.echo(f"🔑 {secrets_path.name} created to hold your secrets.\n")
|
||||
ignore_line = ".secrets.*"
|
||||
comment = "\n# Ignore dynaconf secret files\n"
|
||||
if not gitignore_path.exists():
|
||||
with open(str(gitignore_path), "w", encoding=ENC) as f:
|
||||
f.writelines([comment, ignore_line, "\n"])
|
||||
else:
|
||||
existing = (
|
||||
ignore_line in open(str(gitignore_path), encoding=ENC).read()
|
||||
)
|
||||
if not existing: # pragma: no cover
|
||||
with open(str(gitignore_path), "a+", encoding=ENC) as f:
|
||||
f.writelines([comment, ignore_line, "\n"])
|
||||
|
||||
click.echo(
|
||||
f"🙈 the {secrets_path.name} is also included in `.gitignore` \n"
|
||||
" beware to not push your secrets to a public repo \n"
|
||||
" or use dynaconf builtin support for Vault Servers.\n"
|
||||
)
|
||||
|
||||
if django: # pragma: no cover
|
||||
dj_module, _ = get_module({}, django)
|
||||
dj_filename = dj_module.__file__
|
||||
if Path(dj_filename).exists():
|
||||
click.confirm(
|
||||
f"⁉ {dj_filename} is found do you want to add dynaconf?",
|
||||
abort=True,
|
||||
)
|
||||
with open(dj_filename, "a") as dj_file:
|
||||
dj_file.write(constants.DJANGO_PATCH)
|
||||
click.echo("🎠 Now your Django settings are managed by Dynaconf")
|
||||
else:
|
||||
click.echo("❌ Django settings file not written.")
|
||||
else:
|
||||
click.echo(
|
||||
"🎉 Dynaconf is configured! read more on https://dynaconf.com\n"
|
||||
" Use `dynaconf -i config.settings list` to see your settings\n"
|
||||
)
|
||||
|
||||
|
||||
@main.command(name="get")
|
||||
@click.argument("key", required=True)
|
||||
@click.option(
|
||||
"--default",
|
||||
"-d",
|
||||
default=empty,
|
||||
help="Default value if settings doesn't exist",
|
||||
)
|
||||
@click.option(
|
||||
"--env", "-e", default=None, help="Filters the env to get the values"
|
||||
)
|
||||
@click.option(
|
||||
"--unparse",
|
||||
"-u",
|
||||
default=False,
|
||||
help="Unparse data by adding markers such as @none, @int etc..",
|
||||
is_flag=True,
|
||||
)
|
||||
def get(key, default, env, unparse):
|
||||
"""Returns the raw value for a settings key.
|
||||
|
||||
If result is a dict, list or tuple it is printes as a valid json string.
|
||||
"""
|
||||
if env:
|
||||
env = env.strip()
|
||||
if key:
|
||||
key = key.strip()
|
||||
|
||||
if env:
|
||||
settings.setenv(env)
|
||||
|
||||
if default is not empty:
|
||||
result = settings.get(key, default)
|
||||
else:
|
||||
result = settings[key] # let the keyerror raises
|
||||
|
||||
if unparse:
|
||||
result = unparse_conf_data(result)
|
||||
|
||||
if isinstance(result, (dict, list, tuple)):
|
||||
result = json.dumps(result, sort_keys=True)
|
||||
|
||||
click.echo(result, nl=False)
|
||||
|
||||
|
||||
@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()
|
||||
|
||||
if cur_env == "main":
|
||||
flat = True
|
||||
|
||||
click.echo(
|
||||
click.style(
|
||||
f"Working in {cur_env} environment ",
|
||||
bold=True,
|
||||
bg="bright_blue",
|
||||
fg="bright_white",
|
||||
)
|
||||
)
|
||||
|
||||
if not loader:
|
||||
data = settings.as_dict(env=env, internal=_all)
|
||||
else:
|
||||
identifier = f"{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 "magenta"
|
||||
|
||||
def format_setting(_k, _v):
|
||||
key = click.style(_k, bg=color(_k), fg="bright_white")
|
||||
data_type = click.style(
|
||||
f"<{type(_v).__name__}>", bg="bright_black", fg="bright_white"
|
||||
)
|
||||
value = pprint.pformat(_v)
|
||||
return f"{key}{data_type} {value}"
|
||||
|
||||
if not key:
|
||||
datalines = "\n".join(
|
||||
format_setting(k, v)
|
||||
for k, v in data.items()
|
||||
if k not in data.get("RENAMED_VARS", [])
|
||||
)
|
||||
(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)
|
||||
|
||||
try:
|
||||
value = settings.get(key, empty)
|
||||
except AttributeError:
|
||||
value = empty
|
||||
|
||||
if value is empty:
|
||||
click.echo(click.style("Key not found", bg="red", fg="white"))
|
||||
return
|
||||
|
||||
click.echo(format_setting(key, value))
|
||||
if output:
|
||||
loaders.write(output, {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(f"dynaconf.loaders.{to}_loader")
|
||||
|
||||
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 / f".secrets.{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 / f"settings.{to}"
|
||||
secrets_path = path / f".secrets.{to}"
|
||||
|
||||
if (
|
||||
_vars and not y and settings_path and settings_path.exists()
|
||||
): # pragma: no cover # noqa
|
||||
click.confirm(
|
||||
f"{settings_path} exists do you want to overwrite it?",
|
||||
abort=True,
|
||||
)
|
||||
|
||||
if (
|
||||
_secrets and not y and secrets_path and secrets_path.exists()
|
||||
): # pragma: no cover # noqa
|
||||
click.confirm(
|
||||
f"{secrets_path} exists do you want to overwrite it?",
|
||||
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(f"Data successful written to {settings_path}")
|
||||
|
||||
if _secrets and secrets_path:
|
||||
loader.write(secrets_path, _secrets, merge=True)
|
||||
click.echo(f"Data successful written to {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(f"Data successful written to {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(f"{path} not found", fg="white", bg="red"))
|
||||
sys.exit(1)
|
||||
|
||||
try: # try tomlib first
|
||||
validation_data = tomllib.load(open(str(path), "rb"))
|
||||
except UnicodeDecodeError: # fallback to legacy toml (TBR in 4.0.0)
|
||||
warnings.warn(
|
||||
"TOML files should have only UTF-8 encoded characters. "
|
||||
"starting on 4.0.0 dynaconf will stop allowing invalid chars.",
|
||||
)
|
||||
validation_data = toml.load(
|
||||
open(str(path), encoding=default_settings.ENCODING_FOR_DYNACONF),
|
||||
)
|
||||
|
||||
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(
|
||||
f"Invalid rule for parameter '{name}'",
|
||||
fg="white",
|
||||
bg="yellow",
|
||||
)
|
||||
)
|
||||
else:
|
||||
data.setdefault("env", env)
|
||||
click.echo(
|
||||
click.style(
|
||||
f"Validating '{name}' with '{data}'",
|
||||
fg="white",
|
||||
bg="blue",
|
||||
)
|
||||
)
|
||||
try:
|
||||
Validator(name, **data).validate(settings)
|
||||
except ValidationError as e:
|
||||
click.echo(
|
||||
click.style(f"Error: {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()
|
@ -0,0 +1,52 @@
|
||||
# pragma: no cover
|
||||
from __future__ import annotations
|
||||
|
||||
INI_EXTENSIONS = (".ini", ".conf", ".properties")
|
||||
TOML_EXTENSIONS = (".toml", ".tml")
|
||||
YAML_EXTENSIONS = (".yaml", ".yml")
|
||||
JSON_EXTENSIONS = (".json",)
|
||||
|
||||
ALL_EXTENSIONS = (
|
||||
INI_EXTENSIONS + TOML_EXTENSIONS + YAML_EXTENSIONS + JSON_EXTENSIONS
|
||||
) # noqa
|
||||
|
||||
EXTERNAL_LOADERS = {
|
||||
"ENV": "dynaconf.loaders.env_loader",
|
||||
"VAULT": "dynaconf.loaders.vault_loader",
|
||||
"REDIS": "dynaconf.loaders.redis_loader",
|
||||
}
|
||||
|
||||
DJANGO_PATCH = """
|
||||
# HERE STARTS DYNACONF EXTENSION LOAD (Keep at the very bottom of settings.py)
|
||||
# Read more at https://www.dynaconf.com/django/
|
||||
import dynaconf # noqa
|
||||
settings = dynaconf.DjangoDynaconf(__name__) # noqa
|
||||
# HERE ENDS DYNACONF EXTENSION LOAD (No more code below this line)
|
||||
"""
|
||||
|
||||
INSTANCE_TEMPLATE = """
|
||||
from dynaconf import Dynaconf
|
||||
|
||||
settings = Dynaconf(
|
||||
envvar_prefix="DYNACONF",
|
||||
settings_files={settings_files},
|
||||
)
|
||||
|
||||
# `envvar_prefix` = export envvars with `export DYNACONF_FOO=bar`.
|
||||
# `settings_files` = Load these files in the order.
|
||||
"""
|
||||
|
||||
EXTS = (
|
||||
"py",
|
||||
"toml",
|
||||
"tml",
|
||||
"yaml",
|
||||
"yml",
|
||||
"ini",
|
||||
"conf",
|
||||
"properties",
|
||||
"json",
|
||||
)
|
||||
DEFAULT_SETTINGS_FILES = [f"settings.{ext}" for ext in EXTS] + [
|
||||
f".secrets.{ext}" for ext in EXTS
|
||||
]
|
@ -0,0 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dynaconf.contrib.django_dynaconf_v2 import DjangoDynaconf # noqa
|
||||
from dynaconf.contrib.flask_dynaconf import DynaconfConfig # noqa
|
||||
from dynaconf.contrib.flask_dynaconf import FlaskDynaconf # noqa
|
@ -0,0 +1,142 @@
|
||||
"""Dynaconf django extension
|
||||
|
||||
In the `django_project/settings.py` put at the very bottom of the file:
|
||||
|
||||
# HERE STARTS DYNACONF EXTENSION LOAD (Keep at the very bottom of settings.py)
|
||||
# Read more at https://www.dynaconf.com/django/
|
||||
import dynaconf # noqa
|
||||
settings = dynaconf.DjangoDynaconf(__name__) # noqa
|
||||
# HERE ENDS DYNACONF EXTENSION LOAD (No more code below this line)
|
||||
|
||||
Now in the root of your Django project
|
||||
(the same folder where manage.py is located)
|
||||
|
||||
Put your config files `settings.{py|yaml|toml|ini|json}`
|
||||
and or `.secrets.{py|yaml|toml|ini|json}`
|
||||
|
||||
On your projects root folder now you can start as::
|
||||
|
||||
DJANGO_DEBUG='false' \
|
||||
DJANGO_ALLOWED_HOSTS='["localhost"]' \
|
||||
python manage.py runserver
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
|
||||
import dynaconf
|
||||
|
||||
try: # pragma: no cover
|
||||
from django import conf
|
||||
from django.conf import settings as django_settings
|
||||
|
||||
django_installed = True
|
||||
except ImportError: # pragma: no cover
|
||||
django_installed = False
|
||||
|
||||
|
||||
def load(django_settings_module_name=None, **kwargs): # pragma: no cover
|
||||
if not django_installed:
|
||||
raise RuntimeError(
|
||||
"To use this extension django must be installed "
|
||||
"install it with: pip install django"
|
||||
)
|
||||
|
||||
try:
|
||||
django_settings_module = sys.modules[django_settings_module_name]
|
||||
except KeyError:
|
||||
django_settings_module = sys.modules[
|
||||
os.environ["DJANGO_SETTINGS_MODULE"]
|
||||
]
|
||||
|
||||
settings_module_name = django_settings_module.__name__
|
||||
settings_file = os.path.abspath(django_settings_module.__file__)
|
||||
_root_path = os.path.dirname(settings_file)
|
||||
|
||||
# 1) Create the lazy settings object reusing settings_module consts
|
||||
options = {
|
||||
k.upper(): v
|
||||
for k, v in django_settings_module.__dict__.items()
|
||||
if k.isupper()
|
||||
}
|
||||
options.update(kwargs)
|
||||
options.setdefault(
|
||||
"SKIP_FILES_FOR_DYNACONF", [settings_file, "dynaconf_merge"]
|
||||
)
|
||||
options.setdefault("ROOT_PATH_FOR_DYNACONF", _root_path)
|
||||
options.setdefault("ENVVAR_PREFIX_FOR_DYNACONF", "DJANGO")
|
||||
options.setdefault("ENV_SWITCHER_FOR_DYNACONF", "DJANGO_ENV")
|
||||
options.setdefault("ENVIRONMENTS_FOR_DYNACONF", True)
|
||||
options.setdefault("load_dotenv", True)
|
||||
options.setdefault(
|
||||
"default_settings_paths", dynaconf.DEFAULT_SETTINGS_FILES
|
||||
)
|
||||
|
||||
class UserSettingsHolder(dynaconf.LazySettings):
|
||||
_django_override = True
|
||||
|
||||
lazy_settings = dynaconf.LazySettings(**options)
|
||||
dynaconf.settings = lazy_settings # rebind the settings
|
||||
|
||||
# 2) Set all settings back to django_settings_module for 'django check'
|
||||
lazy_settings.populate_obj(django_settings_module)
|
||||
|
||||
# 3) Bind `settings` and `DYNACONF`
|
||||
setattr(django_settings_module, "settings", lazy_settings)
|
||||
setattr(django_settings_module, "DYNACONF", lazy_settings)
|
||||
|
||||
# 4) keep django original settings
|
||||
dj = {}
|
||||
for key in dir(django_settings):
|
||||
if (
|
||||
key.isupper()
|
||||
and (key != "SETTINGS_MODULE")
|
||||
and key not in lazy_settings.store
|
||||
):
|
||||
dj[key] = getattr(django_settings, key, None)
|
||||
dj["ORIGINAL_SETTINGS_MODULE"] = django_settings.SETTINGS_MODULE
|
||||
|
||||
lazy_settings.update(dj)
|
||||
|
||||
# Allow dynaconf_hooks to be in the same folder as the django.settings
|
||||
dynaconf.loaders.execute_hooks(
|
||||
"post",
|
||||
lazy_settings,
|
||||
lazy_settings.current_env,
|
||||
modules=[settings_module_name],
|
||||
files=[settings_file],
|
||||
)
|
||||
lazy_settings._loaded_py_modules.insert(0, settings_module_name)
|
||||
|
||||
# 5) Patch django.conf.settings
|
||||
class Wrapper:
|
||||
|
||||
# lazy_settings = conf.settings.lazy_settings
|
||||
|
||||
def __getattribute__(self, name):
|
||||
if name == "settings":
|
||||
return lazy_settings
|
||||
if name == "UserSettingsHolder":
|
||||
return UserSettingsHolder
|
||||
return getattr(conf, name)
|
||||
|
||||
# This implementation is recommended by Guido Van Rossum
|
||||
# https://mail.python.org/pipermail/python-ideas/2012-May/014969.html
|
||||
sys.modules["django.conf"] = Wrapper()
|
||||
|
||||
# 6) Enable standalone scripts to use Dynaconf
|
||||
# This is for when `django.conf.settings` is imported directly
|
||||
# on external `scripts` (out of Django's lifetime)
|
||||
for stack_item in reversed(inspect.stack()):
|
||||
if isinstance(
|
||||
stack_item.frame.f_globals.get("settings"), conf.LazySettings
|
||||
):
|
||||
stack_item.frame.f_globals["settings"] = lazy_settings
|
||||
|
||||
return lazy_settings
|
||||
|
||||
|
||||
# syntax sugar
|
||||
DjangoDynaconf = load # noqa
|
@ -0,0 +1,230 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from collections import ChainMap
|
||||
from contextlib import suppress
|
||||
|
||||
try:
|
||||
from flask.config import Config
|
||||
|
||||
flask_installed = True
|
||||
except ImportError: # pragma: no cover
|
||||
flask_installed = False
|
||||
Config = object
|
||||
|
||||
|
||||
import dynaconf
|
||||
import pkg_resources
|
||||
|
||||
|
||||
class FlaskDynaconf:
|
||||
"""The arguments are.
|
||||
app = The created app
|
||||
dynaconf_args = Extra args to be passed to Dynaconf (validator for example)
|
||||
|
||||
All other values are stored as config vars specially::
|
||||
|
||||
ENVVAR_PREFIX_FOR_DYNACONF = env prefix for your envvars to be loaded
|
||||
example:
|
||||
if you set to `MYSITE` then
|
||||
export MYSITE_SQL_PORT='@int 5445'
|
||||
|
||||
with that exported to env you access using:
|
||||
app.config.SQL_PORT
|
||||
app.config.get('SQL_PORT')
|
||||
app.config.get('sql_port')
|
||||
# get is case insensitive
|
||||
app.config['SQL_PORT']
|
||||
|
||||
Dynaconf uses `@int, @bool, @float, @json` to cast
|
||||
env vars
|
||||
|
||||
SETTINGS_FILE_FOR_DYNACONF = The name of the module or file to use as
|
||||
default to load settings. If nothing is
|
||||
passed it will be `settings.*` or value
|
||||
found in `ENVVAR_FOR_DYNACONF`
|
||||
Dynaconf supports
|
||||
.py, .yml, .toml, ini, json
|
||||
|
||||
ATTENTION: Take a look at `settings.yml` and `.secrets.yml` to know the
|
||||
required settings format.
|
||||
|
||||
Settings load order in Dynaconf:
|
||||
|
||||
- Load all defaults and Flask defaults
|
||||
- Load all passed variables when applying FlaskDynaconf
|
||||
- Update with data in settings files
|
||||
- Update with data in environment vars `ENVVAR_FOR_DYNACONF_`
|
||||
|
||||
|
||||
TOML files are very useful to have `envd` settings, lets say,
|
||||
`production` and `development`.
|
||||
|
||||
You can also achieve the same using multiple `.py` files naming as
|
||||
`settings.py`, `production_settings.py` and `development_settings.py`
|
||||
(see examples/validator)
|
||||
|
||||
Example::
|
||||
|
||||
app = Flask(__name__)
|
||||
FlaskDynaconf(
|
||||
app,
|
||||
ENV='MYSITE',
|
||||
SETTINGS_FILE='settings.yml',
|
||||
EXTRA_VALUE='You can add additional config vars here'
|
||||
)
|
||||
|
||||
Take a look at examples/flask in Dynaconf repository
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
app=None,
|
||||
instance_relative_config=False,
|
||||
dynaconf_instance=None,
|
||||
extensions_list=False,
|
||||
**kwargs,
|
||||
):
|
||||
"""kwargs holds initial dynaconf configuration"""
|
||||
if not flask_installed: # pragma: no cover
|
||||
raise RuntimeError(
|
||||
"To use this extension Flask must be installed "
|
||||
"install it with: pip install flask"
|
||||
)
|
||||
self.kwargs = {k.upper(): v for k, v in kwargs.items()}
|
||||
kwargs.setdefault("ENVVAR_PREFIX", "FLASK")
|
||||
env_prefix = f"{kwargs['ENVVAR_PREFIX']}_ENV" # FLASK_ENV
|
||||
kwargs.setdefault("ENV_SWITCHER", env_prefix)
|
||||
kwargs.setdefault("ENVIRONMENTS", True)
|
||||
kwargs.setdefault("load_dotenv", True)
|
||||
kwargs.setdefault(
|
||||
"default_settings_paths", dynaconf.DEFAULT_SETTINGS_FILES
|
||||
)
|
||||
|
||||
self.dynaconf_instance = dynaconf_instance
|
||||
self.instance_relative_config = instance_relative_config
|
||||
self.extensions_list = extensions_list
|
||||
if app:
|
||||
self.init_app(app, **kwargs)
|
||||
|
||||
def init_app(self, app, **kwargs):
|
||||
"""kwargs holds initial dynaconf configuration"""
|
||||
self.kwargs.update(kwargs)
|
||||
self.settings = self.dynaconf_instance or dynaconf.LazySettings(
|
||||
**self.kwargs
|
||||
)
|
||||
dynaconf.settings = self.settings # rebind customized settings
|
||||
app.config = self.make_config(app)
|
||||
app.dynaconf = self.settings
|
||||
|
||||
if self.extensions_list:
|
||||
if not isinstance(self.extensions_list, str):
|
||||
self.extensions_list = "EXTENSIONS"
|
||||
app.config.load_extensions(self.extensions_list)
|
||||
|
||||
def make_config(self, app):
|
||||
root_path = app.root_path
|
||||
if self.instance_relative_config: # pragma: no cover
|
||||
root_path = app.instance_path
|
||||
if self.dynaconf_instance:
|
||||
self.settings.update(self.kwargs)
|
||||
return DynaconfConfig(
|
||||
root_path=root_path,
|
||||
defaults=app.config,
|
||||
_settings=self.settings,
|
||||
_app=app,
|
||||
)
|
||||
|
||||
|
||||
class DynaconfConfig(Config):
|
||||
"""
|
||||
Replacement for flask.config_class that responds as a Dynaconf instance.
|
||||
"""
|
||||
|
||||
def __init__(self, _settings, _app, *args, **kwargs):
|
||||
"""perform the initial load"""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Bring Dynaconf instance value to Flask Config
|
||||
Config.update(self, _settings.store)
|
||||
|
||||
self._settings = _settings
|
||||
self._app = _app
|
||||
|
||||
def __contains__(self, item):
|
||||
return hasattr(self, item)
|
||||
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
return self._settings[key]
|
||||
except KeyError:
|
||||
return Config.__getitem__(self, key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""
|
||||
Allows app.config['key'] = 'foo'
|
||||
"""
|
||||
return self._settings.__setitem__(key, value)
|
||||
|
||||
def _chain_map(self):
|
||||
return ChainMap(self._settings, dict(dict.items(self)))
|
||||
|
||||
def keys(self):
|
||||
return self._chain_map().keys()
|
||||
|
||||
def values(self):
|
||||
return self._chain_map().values()
|
||||
|
||||
def items(self):
|
||||
return self._chain_map().items()
|
||||
|
||||
def setdefault(self, key, value=None):
|
||||
return self._chain_map().setdefault(key, value)
|
||||
|
||||
def __iter__(self):
|
||||
return self._chain_map().__iter__()
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""
|
||||
First try to get value from dynaconf then from Flask Config
|
||||
"""
|
||||
with suppress(AttributeError):
|
||||
return getattr(self._settings, name)
|
||||
|
||||
with suppress(KeyError):
|
||||
return self[name]
|
||||
|
||||
raise AttributeError(
|
||||
f"'{self.__class__.__name__}' object has no attribute '{name}'"
|
||||
)
|
||||
|
||||
def __call__(self, name, *args, **kwargs):
|
||||
return self.get(name, *args, **kwargs)
|
||||
|
||||
def get(self, key, default=None):
|
||||
"""Gets config from dynaconf variables
|
||||
if variables does not exists in dynaconf try getting from
|
||||
`app.config` to support runtime settings."""
|
||||
return self._settings.get(key, Config.get(self, key, default))
|
||||
|
||||
def load_extensions(self, key="EXTENSIONS", app=None):
|
||||
"""Loads flask extensions dynamically."""
|
||||
app = app or self._app
|
||||
extensions = app.config.get(key)
|
||||
if not extensions:
|
||||
warnings.warn(
|
||||
f"Settings is missing {key} to load Flask Extensions",
|
||||
RuntimeWarning,
|
||||
)
|
||||
return
|
||||
|
||||
for object_reference in app.config[key]:
|
||||
# add a placeholder `name` to create a valid entry point
|
||||
entry_point_spec = f"__name = {object_reference}"
|
||||
# parse the entry point specification
|
||||
entry_point = pkg_resources.EntryPoint.parse(entry_point_spec)
|
||||
# dynamically resolve the entry point
|
||||
initializer = entry_point.resolve()
|
||||
# Invoke extension initializer
|
||||
initializer(app)
|
@ -0,0 +1,252 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
from dynaconf.utils import RENAMED_VARS
|
||||
from dynaconf.utils import upperfy
|
||||
from dynaconf.utils import warn_deprecations
|
||||
from dynaconf.utils.files import find_file
|
||||
from dynaconf.utils.parse_conf import parse_conf_data
|
||||
from dynaconf.vendor.dotenv import load_dotenv
|
||||
|
||||
|
||||
def try_renamed(key, value, older_key, current_key):
|
||||
if value is None:
|
||||
if key == current_key:
|
||||
if older_key in os.environ:
|
||||
warnings.warn(
|
||||
f"{older_key} is deprecated please use {current_key}",
|
||||
DeprecationWarning,
|
||||
)
|
||||
value = os.environ[older_key]
|
||||
return value
|
||||
|
||||
|
||||
def get(key, default=None):
|
||||
value = os.environ.get(upperfy(key))
|
||||
|
||||
# compatibility with renamed variables
|
||||
for old, new in RENAMED_VARS.items():
|
||||
value = try_renamed(key, value, old, new)
|
||||
|
||||
return (
|
||||
parse_conf_data(value, tomlfy=True, box_settings={})
|
||||
if value is not None
|
||||
else default
|
||||
)
|
||||
|
||||
|
||||
def start_dotenv(obj=None, root_path=None):
|
||||
# load_from_dotenv_if_installed
|
||||
obj = obj or {}
|
||||
_find_file = getattr(obj, "find_file", find_file)
|
||||
root_path = (
|
||||
root_path
|
||||
or getattr(obj, "_root_path", None)
|
||||
or get("ROOT_PATH_FOR_DYNACONF")
|
||||
)
|
||||
|
||||
dotenv_path = (
|
||||
obj.get("DOTENV_PATH_FOR_DYNACONF")
|
||||
or get("DOTENV_PATH_FOR_DYNACONF")
|
||||
or _find_file(".env", project_root=root_path)
|
||||
)
|
||||
|
||||
load_dotenv(
|
||||
dotenv_path,
|
||||
verbose=obj.get("DOTENV_VERBOSE_FOR_DYNACONF", False),
|
||||
override=obj.get("DOTENV_OVERRIDE_FOR_DYNACONF", False),
|
||||
)
|
||||
|
||||
warn_deprecations(os.environ)
|
||||
|
||||
|
||||
def reload(load_dotenv=None, *args, **kwargs):
|
||||
if load_dotenv:
|
||||
start_dotenv(*args, **kwargs)
|
||||
importlib.reload(sys.modules[__name__])
|
||||
|
||||
|
||||
# default proj root
|
||||
# pragma: no cover
|
||||
ROOT_PATH_FOR_DYNACONF = get("ROOT_PATH_FOR_DYNACONF", None)
|
||||
|
||||
# Default settings file
|
||||
SETTINGS_FILE_FOR_DYNACONF = get("SETTINGS_FILE_FOR_DYNACONF", [])
|
||||
|
||||
# MISPELLS `FILES` when/if it happens
|
||||
mispelled_files = get("SETTINGS_FILES_FOR_DYNACONF", None)
|
||||
if not SETTINGS_FILE_FOR_DYNACONF and mispelled_files is not None:
|
||||
SETTINGS_FILE_FOR_DYNACONF = mispelled_files
|
||||
|
||||
# # ENV SETTINGS
|
||||
# # In dynaconf 1.0.0 `NAMESPACE` got renamed to `ENV`
|
||||
|
||||
|
||||
# If provided environments will be loaded separately
|
||||
ENVIRONMENTS_FOR_DYNACONF = get("ENVIRONMENTS_FOR_DYNACONF", False)
|
||||
MAIN_ENV_FOR_DYNACONF = get("MAIN_ENV_FOR_DYNACONF", "MAIN")
|
||||
|
||||
# If False dynaconf will allow access to first level settings only in upper
|
||||
LOWERCASE_READ_FOR_DYNACONF = get("LOWERCASE_READ_FOR_DYNACONF", True)
|
||||
|
||||
# The environment variable to switch current env
|
||||
ENV_SWITCHER_FOR_DYNACONF = get(
|
||||
"ENV_SWITCHER_FOR_DYNACONF", "ENV_FOR_DYNACONF"
|
||||
)
|
||||
|
||||
# The current env by default is DEVELOPMENT
|
||||
# to switch is needed to `export ENV_FOR_DYNACONF=PRODUCTION`
|
||||
# or put that value in .env file
|
||||
# this value is used only when reading files like .toml|yaml|ini|json
|
||||
ENV_FOR_DYNACONF = get(ENV_SWITCHER_FOR_DYNACONF, "DEVELOPMENT")
|
||||
|
||||
# This variable exists to support `from_env` method
|
||||
FORCE_ENV_FOR_DYNACONF = get("FORCE_ENV_FOR_DYNACONF", None)
|
||||
|
||||
# Default values is taken from DEFAULT pseudo env
|
||||
# this value is used only when reading files like .toml|yaml|ini|json
|
||||
DEFAULT_ENV_FOR_DYNACONF = get("DEFAULT_ENV_FOR_DYNACONF", "DEFAULT")
|
||||
|
||||
# Global values are taken from DYNACONF env used for exported envvars
|
||||
# Values here overwrites all other envs
|
||||
# This namespace is used for files and also envvars
|
||||
ENVVAR_PREFIX_FOR_DYNACONF = get("ENVVAR_PREFIX_FOR_DYNACONF", "DYNACONF")
|
||||
|
||||
# By default all environment variables (filtered by `envvar_prefix`) will
|
||||
# be pulled into settings space. In case some of them are polluting the space,
|
||||
# setting this flag to `True` will change this behaviour.
|
||||
# Only "known" variables will be considered -- that is variables defined before
|
||||
# in settings files (or includes/preloads).
|
||||
IGNORE_UNKNOWN_ENVVARS_FOR_DYNACONF = get(
|
||||
"IGNORE_UNKNOWN_ENVVARS_FOR_DYNACONF", False
|
||||
)
|
||||
|
||||
AUTO_CAST_FOR_DYNACONF = get("AUTO_CAST_FOR_DYNACONF", True)
|
||||
|
||||
# The default encoding to open settings files
|
||||
ENCODING_FOR_DYNACONF = get("ENCODING_FOR_DYNACONF", "utf-8")
|
||||
|
||||
# Merge objects on load
|
||||
MERGE_ENABLED_FOR_DYNACONF = get("MERGE_ENABLED_FOR_DYNACONF", False)
|
||||
|
||||
# Lookup keys considering dots as separators
|
||||
DOTTED_LOOKUP_FOR_DYNACONF = get("DOTTED_LOOKUP_FOR_DYNACONF", True)
|
||||
|
||||
# BY default `__` is the separator for nested env vars
|
||||
# export `DYNACONF__DATABASE__server=server.com`
|
||||
# export `DYNACONF__DATABASE__PORT=6666`
|
||||
# Should result in settings.DATABASE == {'server': 'server.com', 'PORT': 6666}
|
||||
# To disable it one can set `NESTED_SEPARATOR_FOR_DYNACONF=false`
|
||||
NESTED_SEPARATOR_FOR_DYNACONF = get("NESTED_SEPARATOR_FOR_DYNACONF", "__")
|
||||
|
||||
# The env var specifying settings module
|
||||
ENVVAR_FOR_DYNACONF = get("ENVVAR_FOR_DYNACONF", "SETTINGS_FILE_FOR_DYNACONF")
|
||||
|
||||
# Default values for redis configs
|
||||
default_redis = {
|
||||
"host": get("REDIS_HOST_FOR_DYNACONF", "localhost"),
|
||||
"port": int(get("REDIS_PORT_FOR_DYNACONF", 6379)),
|
||||
"db": int(get("REDIS_DB_FOR_DYNACONF", 0)),
|
||||
"decode_responses": get("REDIS_DECODE_FOR_DYNACONF", True),
|
||||
"username": get("REDIS_USERNAME_FOR_DYNACONF", None),
|
||||
"password": get("REDIS_PASSWORD_FOR_DYNACONF", None),
|
||||
}
|
||||
REDIS_FOR_DYNACONF = get("REDIS_FOR_DYNACONF", default_redis)
|
||||
REDIS_ENABLED_FOR_DYNACONF = get("REDIS_ENABLED_FOR_DYNACONF", False)
|
||||
|
||||
# Hashicorp Vault Project
|
||||
vault_scheme = get("VAULT_SCHEME_FOR_DYNACONF", "http")
|
||||
vault_host = get("VAULT_HOST_FOR_DYNACONF", "localhost")
|
||||
vault_port = get("VAULT_PORT_FOR_DYNACONF", "8200")
|
||||
default_vault = {
|
||||
"url": get(
|
||||
"VAULT_URL_FOR_DYNACONF", f"{vault_scheme}://{vault_host}:{vault_port}"
|
||||
),
|
||||
"token": get("VAULT_TOKEN_FOR_DYNACONF", None),
|
||||
"cert": get("VAULT_CERT_FOR_DYNACONF", None),
|
||||
"verify": get("VAULT_VERIFY_FOR_DYNACONF", None),
|
||||
"timeout": get("VAULT_TIMEOUT_FOR_DYNACONF", None),
|
||||
"proxies": get("VAULT_PROXIES_FOR_DYNACONF", None),
|
||||
"allow_redirects": get("VAULT_ALLOW_REDIRECTS_FOR_DYNACONF", None),
|
||||
"namespace": get("VAULT_NAMESPACE_FOR_DYNACONF", None),
|
||||
}
|
||||
VAULT_FOR_DYNACONF = get("VAULT_FOR_DYNACONF", default_vault)
|
||||
VAULT_ENABLED_FOR_DYNACONF = get("VAULT_ENABLED_FOR_DYNACONF", False)
|
||||
VAULT_PATH_FOR_DYNACONF = get("VAULT_PATH_FOR_DYNACONF", "dynaconf")
|
||||
VAULT_MOUNT_POINT_FOR_DYNACONF = get(
|
||||
"VAULT_MOUNT_POINT_FOR_DYNACONF", "secret"
|
||||
)
|
||||
VAULT_ROOT_TOKEN_FOR_DYNACONF = get("VAULT_ROOT_TOKEN_FOR_DYNACONF", None)
|
||||
VAULT_KV_VERSION_FOR_DYNACONF = get("VAULT_KV_VERSION_FOR_DYNACONF", 1)
|
||||
VAULT_AUTH_WITH_IAM_FOR_DYNACONF = get(
|
||||
"VAULT_AUTH_WITH_IAM_FOR_DYNACONF", False
|
||||
)
|
||||
VAULT_AUTH_ROLE_FOR_DYNACONF = get("VAULT_AUTH_ROLE_FOR_DYNACONF", None)
|
||||
VAULT_ROLE_ID_FOR_DYNACONF = get("VAULT_ROLE_ID_FOR_DYNACONF", None)
|
||||
VAULT_SECRET_ID_FOR_DYNACONF = get("VAULT_SECRET_ID_FOR_DYNACONF", None)
|
||||
|
||||
# Only core loaders defined on this list will be invoked
|
||||
core_loaders = ["YAML", "TOML", "INI", "JSON", "PY"]
|
||||
CORE_LOADERS_FOR_DYNACONF = get("CORE_LOADERS_FOR_DYNACONF", core_loaders)
|
||||
|
||||
# External Loaders to read vars from different data stores
|
||||
default_loaders = [
|
||||
"dynaconf.loaders.env_loader",
|
||||
# 'dynaconf.loaders.redis_loader'
|
||||
# 'dynaconf.loaders.vault_loader'
|
||||
]
|
||||
LOADERS_FOR_DYNACONF = get("LOADERS_FOR_DYNACONF", default_loaders)
|
||||
|
||||
# Errors in loaders should be silenced?
|
||||
SILENT_ERRORS_FOR_DYNACONF = get("SILENT_ERRORS_FOR_DYNACONF", True)
|
||||
|
||||
# always fresh variables
|
||||
FRESH_VARS_FOR_DYNACONF = get("FRESH_VARS_FOR_DYNACONF", [])
|
||||
|
||||
DOTENV_PATH_FOR_DYNACONF = get("DOTENV_PATH_FOR_DYNACONF", None)
|
||||
DOTENV_VERBOSE_FOR_DYNACONF = get("DOTENV_VERBOSE_FOR_DYNACONF", False)
|
||||
DOTENV_OVERRIDE_FOR_DYNACONF = get("DOTENV_OVERRIDE_FOR_DYNACONF", False)
|
||||
|
||||
# Currently this is only used by cli. INSTANCE_FOR_DYNACONF specifies python
|
||||
# dotted path to custom LazySettings instance. Last dotted path item should be
|
||||
# instance of LazySettings.
|
||||
INSTANCE_FOR_DYNACONF = get("INSTANCE_FOR_DYNACONF", None)
|
||||
|
||||
# https://msg.pyyaml.org/load
|
||||
YAML_LOADER_FOR_DYNACONF = get("YAML_LOADER_FOR_DYNACONF", "safe_load")
|
||||
|
||||
# Use commentjson? https://commentjson.readthedocs.io/en/latest/
|
||||
COMMENTJSON_ENABLED_FOR_DYNACONF = get(
|
||||
"COMMENTJSON_ENABLED_FOR_DYNACONF", False
|
||||
)
|
||||
|
||||
# Extra file, or list of files where to look for secrets
|
||||
# useful for CI environment like jenkins
|
||||
# where you can export this variable pointing to a local
|
||||
# absolute path of the secrets file.
|
||||
SECRETS_FOR_DYNACONF = get("SECRETS_FOR_DYNACONF", None)
|
||||
|
||||
# To include extra paths based on envvar
|
||||
INCLUDES_FOR_DYNACONF = get("INCLUDES_FOR_DYNACONF", [])
|
||||
|
||||
# To pre-load extra paths based on envvar
|
||||
PRELOAD_FOR_DYNACONF = get("PRELOAD_FOR_DYNACONF", [])
|
||||
|
||||
# Files to skip if found on search tree
|
||||
SKIP_FILES_FOR_DYNACONF = get("SKIP_FILES_FOR_DYNACONF", [])
|
||||
|
||||
# YAML reads empty vars as None, should dynaconf apply validator defaults?
|
||||
# this is set to None, then evaluated on base.Settings.setdefault
|
||||
# possible values are True/False
|
||||
APPLY_DEFAULT_ON_NONE_FOR_DYNACONF = get(
|
||||
"APPLY_DEFAULT_ON_NONE_FOR_DYNACONF", None
|
||||
)
|
||||
|
||||
|
||||
# Backwards compatibility with renamed variables
|
||||
for old, new in RENAMED_VARS.items():
|
||||
setattr(sys.modules[__name__], old, locals()[new])
|
@ -0,0 +1,277 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import os
|
||||
|
||||
from dynaconf import constants as ct
|
||||
from dynaconf import default_settings
|
||||
from dynaconf.loaders import ini_loader
|
||||
from dynaconf.loaders import json_loader
|
||||
from dynaconf.loaders import py_loader
|
||||
from dynaconf.loaders import toml_loader
|
||||
from dynaconf.loaders import yaml_loader
|
||||
from dynaconf.utils import deduplicate
|
||||
from dynaconf.utils import ensure_a_list
|
||||
from dynaconf.utils.boxing import DynaBox
|
||||
from dynaconf.utils.files import get_local_filename
|
||||
from dynaconf.utils.parse_conf import false_values
|
||||
|
||||
|
||||
def default_loader(obj, defaults=None):
|
||||
"""Loads default settings and check if there are overridings
|
||||
exported as environment variables"""
|
||||
defaults = defaults or {}
|
||||
default_settings_values = {
|
||||
key: value
|
||||
for key, value in default_settings.__dict__.items() # noqa
|
||||
if key.isupper()
|
||||
}
|
||||
|
||||
all_keys = deduplicate(
|
||||
list(defaults.keys()) + list(default_settings_values.keys())
|
||||
)
|
||||
|
||||
for key in all_keys:
|
||||
if not obj.exists(key):
|
||||
value = defaults.get(key, default_settings_values.get(key))
|
||||
obj.set(key, value)
|
||||
|
||||
# start dotenv to get default env vars from there
|
||||
# check overrides in env vars
|
||||
if obj.get("load_dotenv") is True:
|
||||
default_settings.start_dotenv(obj)
|
||||
|
||||
# Deal with cases where a custom ENV_SWITCHER_IS_PROVIDED
|
||||
# Example: Flask and Django Extensions
|
||||
env_switcher = defaults.get(
|
||||
"ENV_SWITCHER_FOR_DYNACONF", "ENV_FOR_DYNACONF"
|
||||
)
|
||||
|
||||
for key in all_keys:
|
||||
if key not in default_settings_values.keys():
|
||||
continue
|
||||
|
||||
env_value = obj.get_environ(
|
||||
env_switcher if key == "ENV_FOR_DYNACONF" else key,
|
||||
default="_not_found",
|
||||
)
|
||||
|
||||
if env_value != "_not_found":
|
||||
obj.set(key, env_value, tomlfy=True)
|
||||
|
||||
|
||||
def _run_hook_module(hook, hook_module, obj, key=None):
|
||||
"""Run the hook function from the settings obj.
|
||||
|
||||
given a hook name, a hook_module and a settings object
|
||||
load the function and execute if found.
|
||||
"""
|
||||
if hook in obj._loaded_hooks.get(hook_module.__file__, {}):
|
||||
# already loaded
|
||||
return
|
||||
|
||||
if hook_module and getattr(hook_module, "_error", False):
|
||||
if not isinstance(hook_module._error, FileNotFoundError):
|
||||
raise hook_module._error
|
||||
|
||||
hook_func = getattr(hook_module, hook, None)
|
||||
if hook_func:
|
||||
hook_dict = hook_func(obj.dynaconf.clone())
|
||||
if hook_dict:
|
||||
merge = hook_dict.pop(
|
||||
"dynaconf_merge", hook_dict.pop("DYNACONF_MERGE", False)
|
||||
)
|
||||
if key and key in hook_dict:
|
||||
obj.set(key, hook_dict[key], tomlfy=False, merge=merge)
|
||||
elif not key:
|
||||
obj.update(hook_dict, tomlfy=False, merge=merge)
|
||||
obj._loaded_hooks[hook_module.__file__][hook] = hook_dict
|
||||
|
||||
|
||||
def execute_hooks(
|
||||
hook, obj, env=None, silent=True, key=None, modules=None, files=None
|
||||
):
|
||||
"""Execute dynaconf_hooks from module or filepath."""
|
||||
if hook not in ["post"]:
|
||||
raise ValueError(f"hook {hook} not supported yet.")
|
||||
|
||||
# try to load hooks using python module __name__
|
||||
modules = modules or obj._loaded_py_modules
|
||||
for loaded_module in modules:
|
||||
hook_module_name = ".".join(
|
||||
loaded_module.split(".")[:-1] + ["dynaconf_hooks"]
|
||||
)
|
||||
try:
|
||||
hook_module = importlib.import_module(hook_module_name)
|
||||
except (ImportError, TypeError):
|
||||
# There was no hook on the same path as a python module
|
||||
continue
|
||||
else:
|
||||
_run_hook_module(
|
||||
hook=hook,
|
||||
hook_module=hook_module,
|
||||
obj=obj,
|
||||
key=key,
|
||||
)
|
||||
|
||||
# Try to load from python filename path
|
||||
files = files or obj._loaded_files
|
||||
for loaded_file in files:
|
||||
hook_file = os.path.join(
|
||||
os.path.dirname(loaded_file), "dynaconf_hooks.py"
|
||||
)
|
||||
hook_module = py_loader.import_from_filename(
|
||||
obj, hook_file, silent=silent
|
||||
)
|
||||
if not hook_module:
|
||||
# There was no hook on the same path as a python file
|
||||
continue
|
||||
_run_hook_module(
|
||||
hook=hook,
|
||||
hook_module=hook_module,
|
||||
obj=obj,
|
||||
key=key,
|
||||
)
|
||||
|
||||
|
||||
def settings_loader(
|
||||
obj, settings_module=None, env=None, silent=True, key=None, filename=None
|
||||
):
|
||||
"""Loads from defined settings module
|
||||
|
||||
:param obj: A dynaconf instance
|
||||
:param settings_module: A path or a list of paths e.g settings.toml
|
||||
:param env: Env to look for data defaults: development
|
||||
:param silent: Boolean to raise loading errors
|
||||
:param key: Load a single key if provided
|
||||
:param filename: optional filename to override the settings_module
|
||||
"""
|
||||
if filename is None:
|
||||
settings_module = settings_module or obj.settings_module
|
||||
if not settings_module: # pragma: no cover
|
||||
return
|
||||
files = ensure_a_list(settings_module)
|
||||
else:
|
||||
files = ensure_a_list(filename)
|
||||
|
||||
files.extend(ensure_a_list(obj.get("SECRETS_FOR_DYNACONF", None)))
|
||||
|
||||
found_files = []
|
||||
modules_names = []
|
||||
for item in files:
|
||||
item = str(item) # Ensure str in case of LocalPath/Path is passed.
|
||||
if item.endswith(ct.ALL_EXTENSIONS + (".py",)):
|
||||
p_root = obj._root_path or (
|
||||
os.path.dirname(found_files[0]) if found_files else None
|
||||
)
|
||||
found = obj.find_file(item, project_root=p_root)
|
||||
if found:
|
||||
found_files.append(found)
|
||||
else:
|
||||
# a bare python module name w/o extension
|
||||
modules_names.append(item)
|
||||
|
||||
enabled_core_loaders = [
|
||||
item.upper() for item in obj.get("CORE_LOADERS_FOR_DYNACONF") or []
|
||||
]
|
||||
|
||||
# add `.local.` to found_files list to search for local files.
|
||||
found_files.extend(
|
||||
[
|
||||
get_local_filename(item)
|
||||
for item in found_files
|
||||
if ".local." not in str(item)
|
||||
]
|
||||
)
|
||||
|
||||
for mod_file in modules_names + found_files:
|
||||
# can be set to multiple files settings.py,settings.yaml,...
|
||||
|
||||
# Cascade all loaders
|
||||
loaders = [
|
||||
{"ext": ct.YAML_EXTENSIONS, "name": "YAML", "loader": yaml_loader},
|
||||
{"ext": ct.TOML_EXTENSIONS, "name": "TOML", "loader": toml_loader},
|
||||
{"ext": ct.INI_EXTENSIONS, "name": "INI", "loader": ini_loader},
|
||||
{"ext": ct.JSON_EXTENSIONS, "name": "JSON", "loader": json_loader},
|
||||
]
|
||||
|
||||
for loader in loaders:
|
||||
if loader["name"] not in enabled_core_loaders:
|
||||
continue
|
||||
|
||||
if mod_file.endswith(loader["ext"]):
|
||||
loader["loader"].load(
|
||||
obj, filename=mod_file, env=env, silent=silent, key=key
|
||||
)
|
||||
continue
|
||||
|
||||
if mod_file.endswith(ct.ALL_EXTENSIONS):
|
||||
continue
|
||||
|
||||
if "PY" not in enabled_core_loaders:
|
||||
# pyloader is disabled
|
||||
continue
|
||||
|
||||
# must be Python file or module
|
||||
# load from default defined module settings.py or .secrets.py if exists
|
||||
py_loader.load(obj, mod_file, key=key)
|
||||
|
||||
# load from the current env e.g: development_settings.py
|
||||
env = env or obj.current_env
|
||||
if mod_file.endswith(".py"):
|
||||
if ".secrets.py" == mod_file:
|
||||
tmpl = ".{0}_{1}{2}"
|
||||
mod_file = "secrets.py"
|
||||
else:
|
||||
tmpl = "{0}_{1}{2}"
|
||||
|
||||
dirname = os.path.dirname(mod_file)
|
||||
filename, extension = os.path.splitext(os.path.basename(mod_file))
|
||||
new_filename = tmpl.format(env.lower(), filename, extension)
|
||||
env_mod_file = os.path.join(dirname, new_filename)
|
||||
global_filename = tmpl.format("global", filename, extension)
|
||||
global_mod_file = os.path.join(dirname, global_filename)
|
||||
else:
|
||||
env_mod_file = f"{env.lower()}_{mod_file}"
|
||||
global_mod_file = f"global_{mod_file}"
|
||||
|
||||
py_loader.load(
|
||||
obj,
|
||||
env_mod_file,
|
||||
identifier=f"py_{env.upper()}",
|
||||
silent=True,
|
||||
key=key,
|
||||
)
|
||||
|
||||
# load from global_settings.py
|
||||
py_loader.load(
|
||||
obj, global_mod_file, identifier="py_global", silent=True, key=key
|
||||
)
|
||||
|
||||
|
||||
def enable_external_loaders(obj):
|
||||
"""Enable external service loaders like `VAULT_` and `REDIS_`
|
||||
looks forenv variables like `REDIS_ENABLED_FOR_DYNACONF`
|
||||
"""
|
||||
for name, loader in ct.EXTERNAL_LOADERS.items():
|
||||
enabled = getattr(obj, f"{name.upper()}_ENABLED_FOR_DYNACONF", False)
|
||||
if (
|
||||
enabled
|
||||
and enabled not in false_values
|
||||
and loader not in obj.LOADERS_FOR_DYNACONF
|
||||
): # noqa
|
||||
obj.LOADERS_FOR_DYNACONF.insert(0, loader)
|
||||
|
||||
|
||||
def write(filename, data, env=None):
|
||||
"""Writes `data` to `filename` infers format by file extension."""
|
||||
loader_name = f"{filename.rpartition('.')[-1]}_loader"
|
||||
loader = globals().get(loader_name)
|
||||
if not loader:
|
||||
raise OSError(f"{loader_name} cannot be found.")
|
||||
|
||||
data = DynaBox(data, box_settings={}).to_dict()
|
||||
if loader is not py_loader and env and env not in data:
|
||||
data = {env: data}
|
||||
|
||||
loader.write(filename, data, merge=False)
|
@ -0,0 +1,195 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import warnings
|
||||
|
||||
from dynaconf.utils import build_env_list
|
||||
from dynaconf.utils import ensure_a_list
|
||||
from dynaconf.utils import upperfy
|
||||
|
||||
|
||||
class BaseLoader:
|
||||
"""Base loader for dynaconf source files.
|
||||
|
||||
:param obj: {[LazySettings]} -- [Dynaconf settings]
|
||||
:param env: {[string]} -- [the current env to be loaded defaults to
|
||||
[development]]
|
||||
:param identifier: {[string]} -- [identifier ini, yaml, json, py, toml]
|
||||
:param extensions: {[list]} -- [List of extensions with dots ['.a', '.b']]
|
||||
:param file_reader: {[callable]} -- [reads file return dict]
|
||||
:param string_reader: {[callable]} -- [reads string return dict]
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
obj,
|
||||
env,
|
||||
identifier,
|
||||
extensions,
|
||||
file_reader,
|
||||
string_reader,
|
||||
opener_params=None,
|
||||
):
|
||||
"""Instantiates a loader for different sources"""
|
||||
self.obj = obj
|
||||
self.env = env or obj.current_env
|
||||
self.identifier = identifier
|
||||
self.extensions = extensions
|
||||
self.file_reader = file_reader
|
||||
self.string_reader = string_reader
|
||||
self.opener_params = opener_params or {
|
||||
"mode": "r",
|
||||
"encoding": obj.get("ENCODING_FOR_DYNACONF", "utf-8"),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def warn_not_installed(obj, identifier): # pragma: no cover
|
||||
if identifier not in obj._not_installed_warnings:
|
||||
warnings.warn(
|
||||
f"{identifier} support is not installed in your environment. "
|
||||
f"`pip install dynaconf[{identifier}]`"
|
||||
)
|
||||
obj._not_installed_warnings.append(identifier)
|
||||
|
||||
def load(self, filename=None, key=None, silent=True):
|
||||
"""
|
||||
Reads and loads in to `self.obj` a single key or all keys from source
|
||||
|
||||
:param filename: Optional filename to load
|
||||
:param key: if provided load a single key
|
||||
:param silent: if load errors should be silenced
|
||||
"""
|
||||
|
||||
filename = filename or self.obj.get(self.identifier.upper())
|
||||
if not filename:
|
||||
return
|
||||
|
||||
if not isinstance(filename, (list, tuple)):
|
||||
split_files = ensure_a_list(filename)
|
||||
if all([f.endswith(self.extensions) for f in split_files]): # noqa
|
||||
files = split_files # it is a ['file.ext', ...]
|
||||
else: # it is a single config as string
|
||||
files = [filename]
|
||||
else: # it is already a list/tuple
|
||||
files = filename
|
||||
|
||||
source_data = self.get_source_data(files)
|
||||
|
||||
if self.obj.get("ENVIRONMENTS_FOR_DYNACONF") is False:
|
||||
self._envless_load(source_data, silent, key)
|
||||
else:
|
||||
self._load_all_envs(source_data, silent, key)
|
||||
|
||||
def get_source_data(self, files):
|
||||
"""Reads each file and returns source data for each file
|
||||
{"path/to/file.ext": {"key": "value"}}
|
||||
"""
|
||||
data = {}
|
||||
for source_file in files:
|
||||
if source_file.endswith(self.extensions):
|
||||
try:
|
||||
with open(source_file, **self.opener_params) as open_file:
|
||||
content = self.file_reader(open_file)
|
||||
self.obj._loaded_files.append(source_file)
|
||||
if content:
|
||||
data[source_file] = content
|
||||
except OSError as e:
|
||||
if ".local." not in source_file:
|
||||
warnings.warn(
|
||||
f"{self.identifier}_loader: {source_file} "
|
||||
f":{str(e)}"
|
||||
)
|
||||
else:
|
||||
# for tests it is possible to pass string
|
||||
content = self.string_reader(source_file)
|
||||
if content:
|
||||
data[source_file] = content
|
||||
return data
|
||||
|
||||
def _envless_load(self, source_data, silent=True, key=None):
|
||||
"""Load all the keys from each file without env separation"""
|
||||
for file_data in source_data.values():
|
||||
self._set_data_to_obj(
|
||||
file_data,
|
||||
self.identifier,
|
||||
key=key,
|
||||
)
|
||||
|
||||
def _load_all_envs(self, source_data, silent=True, key=None):
|
||||
"""Load configs from files separating by each environment"""
|
||||
|
||||
for file_data in source_data.values():
|
||||
|
||||
# env name is checked in lower
|
||||
file_data = {k.lower(): value for k, value in file_data.items()}
|
||||
|
||||
# is there a `dynaconf_merge` on top level of file?
|
||||
file_merge = file_data.get("dynaconf_merge")
|
||||
|
||||
# is there a flag disabling dotted lookup on file?
|
||||
file_dotted_lookup = file_data.get("dynaconf_dotted_lookup")
|
||||
|
||||
for env in build_env_list(self.obj, self.env):
|
||||
env = env.lower() # lower for better comparison
|
||||
|
||||
try:
|
||||
data = file_data[env] or {}
|
||||
except KeyError:
|
||||
if silent:
|
||||
continue
|
||||
raise
|
||||
|
||||
if not data:
|
||||
continue
|
||||
|
||||
self._set_data_to_obj(
|
||||
data,
|
||||
f"{self.identifier}_{env}",
|
||||
file_merge,
|
||||
key,
|
||||
file_dotted_lookup=file_dotted_lookup,
|
||||
)
|
||||
|
||||
def _set_data_to_obj(
|
||||
self,
|
||||
data,
|
||||
identifier,
|
||||
file_merge=None,
|
||||
key=False,
|
||||
file_dotted_lookup=None,
|
||||
):
|
||||
"""Calls settings.set to add the keys"""
|
||||
# data 1st level keys should be transformed to upper case.
|
||||
data = {upperfy(k): v for k, v in data.items()}
|
||||
if key:
|
||||
key = upperfy(key)
|
||||
|
||||
if self.obj.filter_strategy:
|
||||
data = self.obj.filter_strategy(data)
|
||||
|
||||
# is there a `dynaconf_merge` inside an `[env]`?
|
||||
file_merge = file_merge or data.pop("DYNACONF_MERGE", False)
|
||||
|
||||
# If not passed or passed as None,
|
||||
# look for inner [env] value, or default settings.
|
||||
if file_dotted_lookup is None:
|
||||
file_dotted_lookup = data.pop(
|
||||
"DYNACONF_DOTTED_LOOKUP",
|
||||
self.obj.get("DOTTED_LOOKUP_FOR_DYNACONF"),
|
||||
)
|
||||
|
||||
if not key:
|
||||
self.obj.update(
|
||||
data,
|
||||
loader_identifier=identifier,
|
||||
merge=file_merge,
|
||||
dotted_lookup=file_dotted_lookup,
|
||||
)
|
||||
elif key in data:
|
||||
self.obj.set(
|
||||
key,
|
||||
data.get(key),
|
||||
loader_identifier=identifier,
|
||||
merge=file_merge,
|
||||
dotted_lookup=file_dotted_lookup,
|
||||
)
|
@ -0,0 +1,108 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from os import environ
|
||||
|
||||
from dynaconf.utils import missing
|
||||
from dynaconf.utils import upperfy
|
||||
from dynaconf.utils.parse_conf import parse_conf_data
|
||||
|
||||
DOTENV_IMPORTED = False
|
||||
try:
|
||||
from dynaconf.vendor.dotenv import cli as dotenv_cli
|
||||
|
||||
DOTENV_IMPORTED = True
|
||||
except ImportError:
|
||||
pass
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
IDENTIFIER = "env"
|
||||
|
||||
|
||||
def load(obj, env=None, silent=True, key=None):
|
||||
"""Loads envvars with prefixes:
|
||||
|
||||
`DYNACONF_` (default global) or `$(ENVVAR_PREFIX_FOR_DYNACONF)_`
|
||||
"""
|
||||
global_prefix = obj.get("ENVVAR_PREFIX_FOR_DYNACONF")
|
||||
if global_prefix is False or global_prefix.upper() != "DYNACONF":
|
||||
load_from_env(obj, "DYNACONF", key, silent, IDENTIFIER + "_global")
|
||||
|
||||
# Load the global env if exists and overwrite everything
|
||||
load_from_env(obj, global_prefix, key, silent, IDENTIFIER + "_global")
|
||||
|
||||
|
||||
def load_from_env(
|
||||
obj,
|
||||
prefix=False,
|
||||
key=None,
|
||||
silent=False,
|
||||
identifier=IDENTIFIER,
|
||||
env=False, # backwards compatibility bc renamed param
|
||||
):
|
||||
if prefix is False and env is not False:
|
||||
prefix = env
|
||||
|
||||
env_ = ""
|
||||
if prefix is not False:
|
||||
if not isinstance(prefix, str):
|
||||
raise TypeError("`prefix/env` must be str or False")
|
||||
|
||||
prefix = prefix.upper()
|
||||
env_ = f"{prefix}_"
|
||||
|
||||
# Load a single environment variable explicitly.
|
||||
if key:
|
||||
key = upperfy(key)
|
||||
value = environ.get(f"{env_}{key}")
|
||||
if value:
|
||||
try: # obj is a Settings
|
||||
obj.set(key, value, loader_identifier=identifier, tomlfy=True)
|
||||
except AttributeError: # obj is a dict
|
||||
obj[key] = parse_conf_data(
|
||||
value, tomlfy=True, box_settings=obj
|
||||
)
|
||||
|
||||
# Load environment variables in bulk (when matching).
|
||||
else:
|
||||
# Only known variables should be loaded from environment?
|
||||
ignore_unknown = obj.get("IGNORE_UNKNOWN_ENVVARS_FOR_DYNACONF")
|
||||
|
||||
trim_len = len(env_)
|
||||
data = {
|
||||
key[trim_len:]: parse_conf_data(
|
||||
data, tomlfy=True, box_settings=obj
|
||||
)
|
||||
for key, data in environ.items()
|
||||
if key.startswith(env_)
|
||||
and not (
|
||||
# Ignore environment variables that haven't been
|
||||
# pre-defined in settings space.
|
||||
ignore_unknown
|
||||
and obj.get(key[trim_len:], default=missing) is missing
|
||||
)
|
||||
}
|
||||
# Update the settings space based on gathered data from environment.
|
||||
if data:
|
||||
filter_strategy = obj.get("FILTER_STRATEGY")
|
||||
if filter_strategy:
|
||||
data = filter_strategy(data)
|
||||
obj.update(data, loader_identifier=identifier)
|
||||
|
||||
|
||||
def write(settings_path, settings_data, **kwargs):
|
||||
"""Write data to .env file"""
|
||||
if not DOTENV_IMPORTED:
|
||||
return
|
||||
for key, value in settings_data.items():
|
||||
quote_mode = (
|
||||
isinstance(value, str)
|
||||
and (value.startswith("'") or value.startswith('"'))
|
||||
) or isinstance(value, (list, dict))
|
||||
dotenv_cli.set_key(
|
||||
str(settings_path),
|
||||
key,
|
||||
str(value),
|
||||
quote_mode="always" if quote_mode else "none",
|
||||
)
|
@ -0,0 +1,62 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
from pathlib import Path
|
||||
|
||||
from dynaconf import default_settings
|
||||
from dynaconf.constants import INI_EXTENSIONS
|
||||
from dynaconf.loaders.base import BaseLoader
|
||||
from dynaconf.utils import object_merge
|
||||
|
||||
try:
|
||||
from configobj import ConfigObj
|
||||
except ImportError: # pragma: no cover
|
||||
ConfigObj = None
|
||||
|
||||
|
||||
def load(obj, env=None, silent=True, key=None, filename=None):
|
||||
"""
|
||||
Reads and loads in to "obj" a single key or all keys from source file.
|
||||
|
||||
:param obj: the settings instance
|
||||
:param env: settings current env default='development'
|
||||
:param silent: if errors should raise
|
||||
:param key: if defined load a single key, else load all in env
|
||||
:param filename: Optional custom filename to load
|
||||
:return: None
|
||||
"""
|
||||
if ConfigObj is None: # pragma: no cover
|
||||
BaseLoader.warn_not_installed(obj, "ini")
|
||||
return
|
||||
|
||||
loader = BaseLoader(
|
||||
obj=obj,
|
||||
env=env,
|
||||
identifier="ini",
|
||||
extensions=INI_EXTENSIONS,
|
||||
file_reader=lambda fileobj: ConfigObj(fileobj).dict(),
|
||||
string_reader=lambda strobj: ConfigObj(strobj.split("\n")).dict(),
|
||||
)
|
||||
loader.load(
|
||||
filename=filename,
|
||||
key=key,
|
||||
silent=silent,
|
||||
)
|
||||
|
||||
|
||||
def write(settings_path, settings_data, merge=True):
|
||||
"""Write data to a settings file.
|
||||
|
||||
:param settings_path: the filepath
|
||||
:param settings_data: a dictionary with data
|
||||
:param merge: boolean if existing file should be merged with new data
|
||||
"""
|
||||
settings_path = Path(settings_path)
|
||||
if settings_path.exists() and merge: # pragma: no cover
|
||||
with open(
|
||||
str(settings_path), encoding=default_settings.ENCODING_FOR_DYNACONF
|
||||
) as open_file:
|
||||
object_merge(ConfigObj(open_file).dict(), settings_data)
|
||||
new = ConfigObj()
|
||||
new.update(settings_data)
|
||||
new.write(open(str(settings_path), "bw"))
|
@ -0,0 +1,80 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from dynaconf import default_settings
|
||||
from dynaconf.constants import JSON_EXTENSIONS
|
||||
from dynaconf.loaders.base import BaseLoader
|
||||
from dynaconf.utils import object_merge
|
||||
from dynaconf.utils.parse_conf import try_to_encode
|
||||
|
||||
try: # pragma: no cover
|
||||
import commentjson
|
||||
except ImportError: # pragma: no cover
|
||||
commentjson = None
|
||||
|
||||
|
||||
def load(obj, env=None, silent=True, key=None, filename=None):
|
||||
"""
|
||||
Reads and loads in to "obj" a single key or all keys from source file.
|
||||
|
||||
:param obj: the settings instance
|
||||
:param env: settings current env default='development'
|
||||
:param silent: if errors should raise
|
||||
:param key: if defined load a single key, else load all in env
|
||||
:param filename: Optional custom filename to load
|
||||
:return: None
|
||||
"""
|
||||
if (
|
||||
obj.get("COMMENTJSON_ENABLED_FOR_DYNACONF") and commentjson
|
||||
): # pragma: no cover # noqa
|
||||
file_reader = commentjson.load
|
||||
string_reader = commentjson.loads
|
||||
else:
|
||||
file_reader = json.load
|
||||
string_reader = json.loads
|
||||
|
||||
loader = BaseLoader(
|
||||
obj=obj,
|
||||
env=env,
|
||||
identifier="json",
|
||||
extensions=JSON_EXTENSIONS,
|
||||
file_reader=file_reader,
|
||||
string_reader=string_reader,
|
||||
)
|
||||
loader.load(
|
||||
filename=filename,
|
||||
key=key,
|
||||
silent=silent,
|
||||
)
|
||||
|
||||
|
||||
def write(settings_path, settings_data, merge=True):
|
||||
"""Write data to a settings file.
|
||||
|
||||
:param settings_path: the filepath
|
||||
:param settings_data: a dictionary with data
|
||||
:param merge: boolean if existing file should be merged with new data
|
||||
"""
|
||||
settings_path = Path(settings_path)
|
||||
if settings_path.exists() and merge: # pragma: no cover
|
||||
with open(
|
||||
str(settings_path), encoding=default_settings.ENCODING_FOR_DYNACONF
|
||||
) as open_file:
|
||||
object_merge(json.load(open_file), settings_data)
|
||||
|
||||
with open(
|
||||
str(settings_path),
|
||||
"w",
|
||||
encoding=default_settings.ENCODING_FOR_DYNACONF,
|
||||
) as open_file:
|
||||
json.dump(settings_data, open_file, cls=DynaconfEncoder)
|
||||
|
||||
|
||||
class DynaconfEncoder(json.JSONEncoder):
|
||||
"""Transform Dynaconf custom types instances to json representation"""
|
||||
|
||||
def default(self, o):
|
||||
return try_to_encode(o, callback=super().default)
|
@ -0,0 +1,148 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import errno
|
||||
import importlib
|
||||
import inspect
|
||||
import io
|
||||
import types
|
||||
from contextlib import suppress
|
||||
from pathlib import Path
|
||||
|
||||
from dynaconf import default_settings
|
||||
from dynaconf.utils import DynaconfDict
|
||||
from dynaconf.utils import object_merge
|
||||
from dynaconf.utils import upperfy
|
||||
from dynaconf.utils.files import find_file
|
||||
|
||||
|
||||
def load(obj, settings_module, identifier="py", silent=False, key=None):
|
||||
"""Tries to import a python module"""
|
||||
mod, loaded_from = get_module(obj, settings_module, silent)
|
||||
if not (mod and loaded_from):
|
||||
return
|
||||
load_from_python_object(obj, mod, settings_module, key, identifier)
|
||||
|
||||
|
||||
def load_from_python_object(
|
||||
obj, mod, settings_module, key=None, identifier=None
|
||||
):
|
||||
file_merge = getattr(mod, "dynaconf_merge", False) or getattr(
|
||||
mod, "DYNACONF_MERGE", False
|
||||
)
|
||||
for setting in dir(mod):
|
||||
# A setting var in a Python file should start with upper case
|
||||
# valid: A_value=1, ABC_value=3 A_BBB__default=1
|
||||
# invalid: a_value=1, MyValue=3
|
||||
# This is to avoid loading functions, classes and built-ins
|
||||
if setting.split("__")[0].isupper():
|
||||
if key is None or key == setting:
|
||||
setting_value = getattr(mod, setting)
|
||||
obj.set(
|
||||
setting,
|
||||
setting_value,
|
||||
loader_identifier=identifier,
|
||||
merge=file_merge,
|
||||
)
|
||||
|
||||
obj._loaded_py_modules.append(mod.__name__)
|
||||
obj._loaded_files.append(mod.__file__)
|
||||
|
||||
|
||||
def try_to_load_from_py_module_name(
|
||||
obj, name, key=None, identifier="py", silent=False
|
||||
):
|
||||
"""Try to load module by its string name.
|
||||
|
||||
Arguments:
|
||||
obj {LAzySettings} -- Dynaconf settings instance
|
||||
name {str} -- Name of the module e.g: foo.bar.zaz
|
||||
|
||||
Keyword Arguments:
|
||||
key {str} -- Single key to be loaded (default: {None})
|
||||
identifier {str} -- Name of identifier to store (default: 'py')
|
||||
silent {bool} -- Weather to raise or silence exceptions.
|
||||
"""
|
||||
ctx = suppress(ImportError, TypeError) if silent else suppress()
|
||||
|
||||
with ctx:
|
||||
mod = importlib.import_module(str(name))
|
||||
load_from_python_object(obj, mod, name, key, identifier)
|
||||
return True # loaded ok!
|
||||
# if it reaches this point that means exception occurred, module not found.
|
||||
return False
|
||||
|
||||
|
||||
def get_module(obj, filename, silent=False):
|
||||
try:
|
||||
mod = importlib.import_module(filename)
|
||||
loaded_from = "module"
|
||||
mod.is_error = False
|
||||
except (ImportError, TypeError):
|
||||
mod = import_from_filename(obj, filename, silent=silent)
|
||||
if mod and not mod._is_error:
|
||||
loaded_from = "filename"
|
||||
else:
|
||||
# it is important to return None in case of not loaded
|
||||
loaded_from = None
|
||||
return mod, loaded_from
|
||||
|
||||
|
||||
def import_from_filename(obj, filename, silent=False): # pragma: no cover
|
||||
"""If settings_module is a filename path import it."""
|
||||
if filename in [item.filename for item in inspect.stack()]:
|
||||
raise ImportError(
|
||||
"Looks like you are loading dynaconf "
|
||||
f"from inside the {filename} file and then it is trying "
|
||||
"to load itself entering in a circular reference "
|
||||
"problem. To solve it you have to "
|
||||
"invoke your program from another root folder "
|
||||
"or rename your program file."
|
||||
)
|
||||
|
||||
_find_file = getattr(obj, "find_file", find_file)
|
||||
if not filename.endswith(".py"):
|
||||
filename = f"{filename}.py"
|
||||
|
||||
if filename in default_settings.SETTINGS_FILE_FOR_DYNACONF:
|
||||
silent = True
|
||||
mod = types.ModuleType(filename.rstrip(".py"))
|
||||
mod.__file__ = filename
|
||||
mod._is_error = False
|
||||
mod._error = None
|
||||
try:
|
||||
with open(
|
||||
_find_file(filename),
|
||||
encoding=default_settings.ENCODING_FOR_DYNACONF,
|
||||
) as config_file:
|
||||
exec(compile(config_file.read(), filename, "exec"), mod.__dict__)
|
||||
except OSError as e:
|
||||
e.strerror = (
|
||||
f"py_loader: error loading file " f"({e.strerror} {filename})\n"
|
||||
)
|
||||
if silent and e.errno in (errno.ENOENT, errno.EISDIR):
|
||||
return
|
||||
mod._is_error = True
|
||||
mod._error = e
|
||||
return mod
|
||||
|
||||
|
||||
def write(settings_path, settings_data, merge=True):
|
||||
"""Write data to a settings file.
|
||||
|
||||
:param settings_path: the filepath
|
||||
:param settings_data: a dictionary with data
|
||||
:param merge: boolean if existing file should be merged with new data
|
||||
"""
|
||||
settings_path = Path(settings_path)
|
||||
if settings_path.exists() and merge: # pragma: no cover
|
||||
existing = DynaconfDict()
|
||||
load(existing, str(settings_path))
|
||||
object_merge(existing, settings_data)
|
||||
with open(
|
||||
str(settings_path),
|
||||
"w",
|
||||
encoding=default_settings.ENCODING_FOR_DYNACONF,
|
||||
) as f:
|
||||
f.writelines(
|
||||
[f"{upperfy(k)} = {repr(v)}\n" for k, v in settings_data.items()]
|
||||
)
|
@ -0,0 +1,108 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dynaconf.utils import build_env_list
|
||||
from dynaconf.utils import upperfy
|
||||
from dynaconf.utils.parse_conf import parse_conf_data
|
||||
from dynaconf.utils.parse_conf import unparse_conf_data
|
||||
|
||||
try:
|
||||
from redis import StrictRedis
|
||||
except ImportError:
|
||||
StrictRedis = None
|
||||
|
||||
IDENTIFIER = "redis"
|
||||
|
||||
|
||||
def load(obj, env=None, silent=True, key=None):
|
||||
"""Reads and loads in to "settings" a single key or all keys from redis
|
||||
|
||||
:param obj: the settings instance
|
||||
:param env: settings env default='DYNACONF'
|
||||
:param silent: if errors should raise
|
||||
:param key: if defined load a single key, else load all in env
|
||||
:return: None
|
||||
"""
|
||||
if StrictRedis is None:
|
||||
raise ImportError(
|
||||
"redis package is not installed in your environment. "
|
||||
"`pip install dynaconf[redis]` or disable the redis loader with "
|
||||
"export REDIS_ENABLED_FOR_DYNACONF=false"
|
||||
)
|
||||
|
||||
redis = StrictRedis(**obj.get("REDIS_FOR_DYNACONF"))
|
||||
prefix = obj.get("ENVVAR_PREFIX_FOR_DYNACONF")
|
||||
# prefix is added to env_list to keep backwards compatibility
|
||||
env_list = [prefix] + build_env_list(obj, env or obj.current_env)
|
||||
for env_name in env_list:
|
||||
holder = f"{prefix.upper()}_{env_name.upper()}"
|
||||
try:
|
||||
if key:
|
||||
value = redis.hget(holder.upper(), key)
|
||||
if value:
|
||||
parsed_value = parse_conf_data(
|
||||
value, tomlfy=True, box_settings=obj
|
||||
)
|
||||
if parsed_value:
|
||||
obj.set(key, parsed_value)
|
||||
else:
|
||||
data = {
|
||||
key: parse_conf_data(value, tomlfy=True, box_settings=obj)
|
||||
for key, value in redis.hgetall(holder.upper()).items()
|
||||
}
|
||||
if data:
|
||||
obj.update(data, loader_identifier=IDENTIFIER)
|
||||
except Exception:
|
||||
if silent:
|
||||
return False
|
||||
raise
|
||||
|
||||
|
||||
def write(obj, data=None, **kwargs):
|
||||
"""Write a value in to loader source
|
||||
|
||||
:param obj: settings object
|
||||
:param data: vars to be stored
|
||||
:param kwargs: vars to be stored
|
||||
:return:
|
||||
"""
|
||||
if obj.REDIS_ENABLED_FOR_DYNACONF is False:
|
||||
raise RuntimeError(
|
||||
"Redis is not configured \n"
|
||||
"export REDIS_ENABLED_FOR_DYNACONF=true\n"
|
||||
"and configure the REDIS_*_FOR_DYNACONF variables"
|
||||
)
|
||||
client = StrictRedis(**obj.REDIS_FOR_DYNACONF)
|
||||
holder = obj.get("ENVVAR_PREFIX_FOR_DYNACONF").upper()
|
||||
# add env to holder
|
||||
holder = f"{holder}_{obj.current_env.upper()}"
|
||||
|
||||
data = data or {}
|
||||
data.update(kwargs)
|
||||
if not data:
|
||||
raise AttributeError("Data must be provided")
|
||||
redis_data = {
|
||||
upperfy(key): unparse_conf_data(value) for key, value in data.items()
|
||||
}
|
||||
client.hmset(holder.upper(), redis_data)
|
||||
load(obj)
|
||||
|
||||
|
||||
def delete(obj, key=None):
|
||||
"""
|
||||
Delete a single key if specified, or all env if key is none
|
||||
:param obj: settings object
|
||||
:param key: key to delete from store location
|
||||
:return: None
|
||||
"""
|
||||
client = StrictRedis(**obj.REDIS_FOR_DYNACONF)
|
||||
holder = obj.get("ENVVAR_PREFIX_FOR_DYNACONF").upper()
|
||||
# add env to holder
|
||||
holder = f"{holder}_{obj.current_env.upper()}"
|
||||
|
||||
if key:
|
||||
client.hdel(holder.upper(), upperfy(key))
|
||||
obj.unset(key)
|
||||
else:
|
||||
keys = client.hkeys(holder.upper())
|
||||
client.delete(holder.upper())
|
||||
obj.unset_all(keys)
|
@ -0,0 +1,122 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
|
||||
from dynaconf import default_settings
|
||||
from dynaconf.constants import TOML_EXTENSIONS
|
||||
from dynaconf.loaders.base import BaseLoader
|
||||
from dynaconf.utils import object_merge
|
||||
from dynaconf.vendor import toml # Backwards compatibility with uiri/toml
|
||||
from dynaconf.vendor import tomllib # New tomllib stdlib on py3.11
|
||||
|
||||
|
||||
def load(obj, env=None, silent=True, key=None, filename=None):
|
||||
"""
|
||||
Reads and loads in to "obj" a single key or all keys from source file.
|
||||
|
||||
:param obj: the settings instance
|
||||
:param env: settings current env default='development'
|
||||
:param silent: if errors should raise
|
||||
:param key: if defined load a single key, else load all in env
|
||||
:param filename: Optional custom filename to load
|
||||
:return: None
|
||||
"""
|
||||
|
||||
try:
|
||||
loader = BaseLoader(
|
||||
obj=obj,
|
||||
env=env,
|
||||
identifier="toml",
|
||||
extensions=TOML_EXTENSIONS,
|
||||
file_reader=tomllib.load,
|
||||
string_reader=tomllib.loads,
|
||||
opener_params={"mode": "rb"},
|
||||
)
|
||||
loader.load(
|
||||
filename=filename,
|
||||
key=key,
|
||||
silent=silent,
|
||||
)
|
||||
except UnicodeDecodeError: # pragma: no cover
|
||||
"""
|
||||
NOTE: Compat functions exists to keep backwards compatibility with
|
||||
the new tomllib library. The old library was called `toml` and
|
||||
the new one is called `tomllib`.
|
||||
|
||||
The old lib uiri/toml allowed unicode characters and re-added files
|
||||
as string.
|
||||
|
||||
The new tomllib (stdlib) does not allow unicode characters, only
|
||||
utf-8 encoded, and read files as binary.
|
||||
|
||||
NOTE: In dynaconf 4.0.0 we will drop support for the old library
|
||||
removing the compat functions and calling directly the new lib.
|
||||
"""
|
||||
loader = BaseLoader(
|
||||
obj=obj,
|
||||
env=env,
|
||||
identifier="toml",
|
||||
extensions=TOML_EXTENSIONS,
|
||||
file_reader=toml.load,
|
||||
string_reader=toml.loads,
|
||||
)
|
||||
loader.load(
|
||||
filename=filename,
|
||||
key=key,
|
||||
silent=silent,
|
||||
)
|
||||
|
||||
warnings.warn(
|
||||
"TOML files should have only UTF-8 encoded characters. "
|
||||
"starting on 4.0.0 dynaconf will stop allowing invalid chars.",
|
||||
)
|
||||
|
||||
|
||||
def write(settings_path, settings_data, merge=True):
|
||||
"""Write data to a settings file.
|
||||
|
||||
:param settings_path: the filepath
|
||||
:param settings_data: a dictionary with data
|
||||
:param merge: boolean if existing file should be merged with new data
|
||||
"""
|
||||
settings_path = Path(settings_path)
|
||||
if settings_path.exists() and merge: # pragma: no cover
|
||||
try: # tomllib first
|
||||
with open(str(settings_path), "rb") as open_file:
|
||||
object_merge(tomllib.load(open_file), settings_data)
|
||||
except UnicodeDecodeError: # pragma: no cover
|
||||
# uiri/toml fallback (TBR on 4.0.0)
|
||||
with open(
|
||||
str(settings_path),
|
||||
encoding=default_settings.ENCODING_FOR_DYNACONF,
|
||||
) as open_file:
|
||||
object_merge(toml.load(open_file), settings_data)
|
||||
|
||||
try: # tomllib first
|
||||
with open(str(settings_path), "wb") as open_file:
|
||||
tomllib.dump(encode_nulls(settings_data), open_file)
|
||||
except UnicodeEncodeError: # pragma: no cover
|
||||
# uiri/toml fallback (TBR on 4.0.0)
|
||||
with open(
|
||||
str(settings_path),
|
||||
"w",
|
||||
encoding=default_settings.ENCODING_FOR_DYNACONF,
|
||||
) as open_file:
|
||||
toml.dump(encode_nulls(settings_data), open_file)
|
||||
|
||||
warnings.warn(
|
||||
"TOML files should have only UTF-8 encoded characters. "
|
||||
"starting on 4.0.0 dynaconf will stop allowing invalid chars.",
|
||||
)
|
||||
|
||||
|
||||
def encode_nulls(data):
|
||||
"""TOML does not support `None` so this function transforms to '@none '."""
|
||||
if data is None:
|
||||
return "@none "
|
||||
if isinstance(data, dict):
|
||||
return {key: encode_nulls(value) for key, value in data.items()}
|
||||
elif isinstance(data, (list, tuple)):
|
||||
return [encode_nulls(item) for item in data]
|
||||
return data
|
@ -0,0 +1,186 @@
|
||||
# docker run -e 'VAULT_DEV_ROOT_TOKEN_ID=myroot' -p 8200:8200 vault
|
||||
# pip install hvac
|
||||
from __future__ import annotations
|
||||
|
||||
from dynaconf.utils import build_env_list
|
||||
from dynaconf.utils.parse_conf import parse_conf_data
|
||||
|
||||
try:
|
||||
import boto3
|
||||
except ImportError:
|
||||
boto3 = None
|
||||
|
||||
try:
|
||||
from hvac import Client
|
||||
from hvac.exceptions import InvalidPath
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"vault package is not installed in your environment. "
|
||||
"`pip install dynaconf[vault]` or disable the vault loader with "
|
||||
"export VAULT_ENABLED_FOR_DYNACONF=false"
|
||||
)
|
||||
|
||||
|
||||
IDENTIFIER = "vault"
|
||||
|
||||
|
||||
# backwards compatibility
|
||||
_get_env_list = build_env_list
|
||||
|
||||
|
||||
def get_client(obj):
|
||||
client = Client(
|
||||
**{k: v for k, v in obj.VAULT_FOR_DYNACONF.items() if v is not None}
|
||||
)
|
||||
if obj.VAULT_ROLE_ID_FOR_DYNACONF is not None:
|
||||
client.auth.approle.login(
|
||||
role_id=obj.VAULT_ROLE_ID_FOR_DYNACONF,
|
||||
secret_id=obj.get("VAULT_SECRET_ID_FOR_DYNACONF"),
|
||||
)
|
||||
elif obj.VAULT_ROOT_TOKEN_FOR_DYNACONF is not None:
|
||||
client.token = obj.VAULT_ROOT_TOKEN_FOR_DYNACONF
|
||||
elif obj.VAULT_AUTH_WITH_IAM_FOR_DYNACONF:
|
||||
if boto3 is None:
|
||||
raise ImportError(
|
||||
"boto3 package is not installed in your environment. "
|
||||
"`pip install boto3` or disable the VAULT_AUTH_WITH_IAM"
|
||||
)
|
||||
|
||||
session = boto3.Session()
|
||||
credentials = session.get_credentials()
|
||||
client.auth.aws.iam_login(
|
||||
credentials.access_key,
|
||||
credentials.secret_key,
|
||||
credentials.token,
|
||||
role=obj.VAULT_AUTH_ROLE_FOR_DYNACONF,
|
||||
)
|
||||
assert client.is_authenticated(), (
|
||||
"Vault authentication error: is VAULT_TOKEN_FOR_DYNACONF or "
|
||||
"VAULT_ROLE_ID_FOR_DYNACONF defined?"
|
||||
)
|
||||
client.secrets.kv.default_kv_version = obj.VAULT_KV_VERSION_FOR_DYNACONF
|
||||
return client
|
||||
|
||||
|
||||
def load(obj, env=None, silent=None, key=None):
|
||||
"""Reads and loads in to "settings" a single key or all keys from vault
|
||||
|
||||
:param obj: the settings instance
|
||||
:param env: settings env default='DYNACONF'
|
||||
:param silent: if errors should raise
|
||||
:param key: if defined load a single key, else load all in env
|
||||
:return: None
|
||||
"""
|
||||
client = get_client(obj)
|
||||
try:
|
||||
if obj.VAULT_KV_VERSION_FOR_DYNACONF == 2:
|
||||
dirs = client.secrets.kv.v2.list_secrets(
|
||||
path=obj.VAULT_PATH_FOR_DYNACONF,
|
||||
mount_point=obj.VAULT_MOUNT_POINT_FOR_DYNACONF,
|
||||
)["data"]["keys"]
|
||||
else:
|
||||
dirs = client.secrets.kv.v1.list_secrets(
|
||||
path=obj.VAULT_PATH_FOR_DYNACONF,
|
||||
mount_point=obj.VAULT_MOUNT_POINT_FOR_DYNACONF,
|
||||
)["data"]["keys"]
|
||||
except InvalidPath:
|
||||
# The given path is not a directory
|
||||
dirs = []
|
||||
# First look for secrets into environments less store
|
||||
if not obj.ENVIRONMENTS_FOR_DYNACONF:
|
||||
# By adding '', dynaconf will now read secrets from environments-less
|
||||
# store which are not written by `dynaconf write` to Vault store
|
||||
env_list = [obj.MAIN_ENV_FOR_DYNACONF.lower(), ""]
|
||||
# Finally, look for secret into all the environments
|
||||
else:
|
||||
env_list = dirs + build_env_list(obj, env)
|
||||
for env in env_list:
|
||||
path = "/".join([obj.VAULT_PATH_FOR_DYNACONF, env])
|
||||
try:
|
||||
if obj.VAULT_KV_VERSION_FOR_DYNACONF == 2:
|
||||
data = client.secrets.kv.v2.read_secret_version(
|
||||
path, mount_point=obj.VAULT_MOUNT_POINT_FOR_DYNACONF
|
||||
)
|
||||
else:
|
||||
data = client.secrets.kv.read_secret(
|
||||
"data/" + path,
|
||||
mount_point=obj.VAULT_MOUNT_POINT_FOR_DYNACONF,
|
||||
)
|
||||
except InvalidPath:
|
||||
# If the path doesn't exist, ignore it and set data to None
|
||||
data = None
|
||||
if data:
|
||||
# There seems to be a data dict within a data dict,
|
||||
# extract the inner data
|
||||
data = data.get("data", {}).get("data", {})
|
||||
try:
|
||||
if (
|
||||
obj.VAULT_KV_VERSION_FOR_DYNACONF == 2
|
||||
and obj.ENVIRONMENTS_FOR_DYNACONF
|
||||
and data
|
||||
):
|
||||
data = data.get("data", {})
|
||||
if data and key:
|
||||
value = parse_conf_data(
|
||||
data.get(key), tomlfy=True, box_settings=obj
|
||||
)
|
||||
if value:
|
||||
obj.set(key, value)
|
||||
elif data:
|
||||
obj.update(data, loader_identifier=IDENTIFIER, tomlfy=True)
|
||||
except Exception:
|
||||
if silent:
|
||||
return False
|
||||
raise
|
||||
|
||||
|
||||
def write(obj, data=None, **kwargs):
|
||||
"""Write a value in to loader source
|
||||
|
||||
:param obj: settings object
|
||||
:param data: vars to be stored
|
||||
:param kwargs: vars to be stored
|
||||
:return:
|
||||
"""
|
||||
if obj.VAULT_ENABLED_FOR_DYNACONF is False:
|
||||
raise RuntimeError(
|
||||
"Vault is not configured \n"
|
||||
"export VAULT_ENABLED_FOR_DYNACONF=true\n"
|
||||
"and configure the VAULT_FOR_DYNACONF_* variables"
|
||||
)
|
||||
data = data or {}
|
||||
data.update(kwargs)
|
||||
if not data:
|
||||
raise AttributeError("Data must be provided")
|
||||
data = {"data": data}
|
||||
client = get_client(obj)
|
||||
if obj.VAULT_KV_VERSION_FOR_DYNACONF == 1:
|
||||
mount_point = obj.VAULT_MOUNT_POINT_FOR_DYNACONF + "/data"
|
||||
else:
|
||||
mount_point = obj.VAULT_MOUNT_POINT_FOR_DYNACONF
|
||||
path = "/".join([obj.VAULT_PATH_FOR_DYNACONF, obj.current_env.lower()])
|
||||
client.secrets.kv.create_or_update_secret(
|
||||
path, secret=data, mount_point=mount_point
|
||||
)
|
||||
load(obj)
|
||||
|
||||
|
||||
def list_envs(obj, path=""):
|
||||
"""
|
||||
This function is a helper to get a list of all the existing envs in
|
||||
the source of data, the use case is:
|
||||
existing_envs = vault_loader.list_envs(settings)
|
||||
for env in exiting_envs:
|
||||
with settings.using_env(env): # switch to the env
|
||||
# do something with a key of that env
|
||||
|
||||
:param obj: settings object
|
||||
:param path: path to the vault secrets
|
||||
:return: list containing all the keys at the given path
|
||||
"""
|
||||
client = get_client(obj)
|
||||
path = path or obj.get("VAULT_PATH_FOR_DYNACONF")
|
||||
try:
|
||||
return client.list(f"/secret/metadata/{path}")["data"]["keys"]
|
||||
except TypeError:
|
||||
return []
|
@ -0,0 +1,87 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
from pathlib import Path
|
||||
from warnings import warn
|
||||
|
||||
from dynaconf import default_settings
|
||||
from dynaconf.constants import YAML_EXTENSIONS
|
||||
from dynaconf.loaders.base import BaseLoader
|
||||
from dynaconf.utils import object_merge
|
||||
from dynaconf.utils.parse_conf import try_to_encode
|
||||
from dynaconf.vendor.ruamel import yaml
|
||||
|
||||
# Add support for Dynaconf Lazy values to YAML dumper
|
||||
yaml.SafeDumper.yaml_representers[
|
||||
None
|
||||
] = lambda self, data: yaml.representer.SafeRepresenter.represent_str(
|
||||
self, try_to_encode(data)
|
||||
)
|
||||
|
||||
|
||||
def load(obj, env=None, silent=True, key=None, filename=None):
|
||||
"""
|
||||
Reads and loads in to "obj" a single key or all keys from source file.
|
||||
|
||||
:param obj: the settings instance
|
||||
:param env: settings current env default='development'
|
||||
:param silent: if errors should raise
|
||||
:param key: if defined load a single key, else load all in env
|
||||
:param filename: Optional custom filename to load
|
||||
:return: None
|
||||
"""
|
||||
# Resolve the loaders
|
||||
# https://github.com/yaml/pyyaml/wiki/PyYAML-yaml.load(input)-Deprecation
|
||||
# Possible values are `safe_load, full_load, unsafe_load, load`
|
||||
yaml_reader = getattr(
|
||||
yaml, obj.get("YAML_LOADER_FOR_DYNACONF"), yaml.safe_load
|
||||
)
|
||||
if yaml_reader.__name__ == "unsafe_load": # pragma: no cover
|
||||
warn(
|
||||
"yaml.unsafe_load is deprecated."
|
||||
" Please read https://msg.pyyaml.org/load for full details."
|
||||
" Try to use full_load or safe_load."
|
||||
)
|
||||
|
||||
loader = BaseLoader(
|
||||
obj=obj,
|
||||
env=env,
|
||||
identifier="yaml",
|
||||
extensions=YAML_EXTENSIONS,
|
||||
file_reader=yaml_reader,
|
||||
string_reader=yaml_reader,
|
||||
)
|
||||
loader.load(
|
||||
filename=filename,
|
||||
key=key,
|
||||
silent=silent,
|
||||
)
|
||||
|
||||
|
||||
def write(settings_path, settings_data, merge=True):
|
||||
"""Write data to a settings file.
|
||||
|
||||
:param settings_path: the filepath
|
||||
:param settings_data: a dictionary with data
|
||||
:param merge: boolean if existing file should be merged with new data
|
||||
"""
|
||||
settings_path = Path(settings_path)
|
||||
if settings_path.exists() and merge: # pragma: no cover
|
||||
with open(
|
||||
str(settings_path), encoding=default_settings.ENCODING_FOR_DYNACONF
|
||||
) as open_file:
|
||||
object_merge(yaml.safe_load(open_file), settings_data)
|
||||
|
||||
with open(
|
||||
str(settings_path),
|
||||
"w",
|
||||
encoding=default_settings.ENCODING_FOR_DYNACONF,
|
||||
) as open_file:
|
||||
yaml.dump(
|
||||
settings_data,
|
||||
open_file,
|
||||
Dumper=yaml.dumper.SafeDumper,
|
||||
explicit_start=True,
|
||||
indent=2,
|
||||
default_flow_style=False,
|
||||
)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue