bazarr/libs/dynaconf/contrib/flask_dynaconf.py

231 lines
7.4 KiB
Python

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)