diff options
Diffstat (limited to 'libs/dynaconf/contrib/flask_dynaconf.py')
-rw-r--r-- | libs/dynaconf/contrib/flask_dynaconf.py | 230 |
1 files changed, 230 insertions, 0 deletions
diff --git a/libs/dynaconf/contrib/flask_dynaconf.py b/libs/dynaconf/contrib/flask_dynaconf.py new file mode 100644 index 000000000..a305194a2 --- /dev/null +++ b/libs/dynaconf/contrib/flask_dynaconf.py @@ -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) |