266 lines
8.7 KiB
Python
266 lines
8.7 KiB
Python
import importlib
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
from configparser import ConfigParser
|
|
from datetime import timedelta
|
|
from typing import Any, Callable, Dict, Mapping, Optional, Union
|
|
|
|
from .typing import FilePath
|
|
from .utils import file_path_to_path
|
|
|
|
|
|
DEFAULT_CONFIG = {
|
|
'APPLICATION_ROOT': None,
|
|
'BODY_TIMEOUT': 60, # Second
|
|
'DEBUG': None,
|
|
'ENV': None,
|
|
'JSON_AS_ASCII': True,
|
|
'JSON_SORT_KEYS': True,
|
|
'JSONIFY_MIMETYPE': 'application/json',
|
|
'JSONIFY_PRETTYPRINT_REGULAR': False,
|
|
'MAX_CONTENT_LENGTH': 16 * 1024 * 1024, # 16 MB Limit
|
|
'PERMANENT_SESSION_LIFETIME': timedelta(days=31),
|
|
'PREFERRED_URL_SCHEME': 'http',
|
|
'PROPAGATE_EXCEPTIONS': None,
|
|
'RESPONSE_TIMEOUT': 60, # Second
|
|
'SECRET_KEY': None,
|
|
'SEND_FILE_MAX_AGE_DEFAULT': timedelta(hours=12),
|
|
'SERVER_NAME': None,
|
|
'SESSION_COOKIE_DOMAIN': None,
|
|
'SESSION_COOKIE_HTTPONLY': True,
|
|
'SESSION_COOKIE_NAME': 'session',
|
|
'SESSION_COOKIE_PATH': None,
|
|
'SESSION_COOKIE_SECURE': False,
|
|
'SESSION_REFRESH_EACH_REQUEST': True,
|
|
'TEMPLATES_AUTO_RELOAD': None,
|
|
'TESTING': False,
|
|
'TRAP_HTTP_EXCEPTIONS': False,
|
|
}
|
|
|
|
|
|
class ConfigAttribute:
|
|
"""Implements a property descriptor for objects with a config attribute.
|
|
|
|
When used as a class instance it will look up the key on the class
|
|
config object, for example:
|
|
|
|
.. code-block:: python
|
|
|
|
class Object:
|
|
config = {}
|
|
foo = ConfigAttribute('foo')
|
|
|
|
obj = Object()
|
|
obj.foo = 'bob'
|
|
assert obj.foo == obj.config['foo']
|
|
"""
|
|
|
|
def __init__(self, key: str, converter: Optional[Callable]=None) -> None:
|
|
self.key = key
|
|
self.converter = converter
|
|
|
|
def __get__(self, instance: object, owner: type=None) -> Any:
|
|
if instance is None:
|
|
return self
|
|
result = instance.config[self.key] # type: ignore
|
|
if self.converter is not None:
|
|
return self.converter(result)
|
|
else:
|
|
return result
|
|
|
|
def __set__(self, instance: object, value: Any) -> None:
|
|
instance.config[self.key] = value # type: ignore
|
|
|
|
|
|
class Config(dict):
|
|
"""Extends a standard Python dictionary with additional load (from) methods.
|
|
|
|
Note that the convention (as enforced when loading) is that
|
|
configuration keys are upper case. Whilst you can set lower case
|
|
keys it is not recommended.
|
|
"""
|
|
|
|
def __init__(self, root_path: FilePath, defaults: Optional[dict]=None) -> None:
|
|
super().__init__(defaults or {})
|
|
self.root_path = file_path_to_path(root_path)
|
|
|
|
def from_envvar(self, variable_name: str, silent: bool=False) -> None:
|
|
"""Load the configuration from a location specified in the environment.
|
|
|
|
This will load a cfg file using :meth:`from_pyfile` from the
|
|
location specified in the environment, for example the two blocks
|
|
below are equivalent.
|
|
|
|
.. code-block:: python
|
|
|
|
app.config.from_envvar('CONFIG')
|
|
|
|
.. code-block:: python
|
|
|
|
filename = os.environ['CONFIG']
|
|
app.config.from_pyfile(filename)
|
|
"""
|
|
value = os.environ.get(variable_name)
|
|
if value is None and not silent:
|
|
raise RuntimeError(
|
|
f"Environment variable {variable_name} is not present, cannot load config",
|
|
)
|
|
return self.from_pyfile(value)
|
|
|
|
def from_pyfile(self, filename: str, silent: bool=False) -> None:
|
|
"""Load the configuration from a Python cfg or py file.
|
|
|
|
See Python's ConfigParser docs for details on the cfg format.
|
|
It is a common practice to load the defaults from the source
|
|
using the :meth:`from_object` and then override with a cfg or
|
|
py file, for example
|
|
|
|
.. code-block:: python
|
|
|
|
app.config.from_object('config_module')
|
|
app.config.from_pyfile('production.cfg')
|
|
|
|
Arguments:
|
|
filename: The filename which when appended to
|
|
:attr:`root_path` gives the path to the file
|
|
|
|
"""
|
|
file_path = self.root_path / filename
|
|
try:
|
|
spec = importlib.util.spec_from_file_location("module.name", file_path) # type: ignore
|
|
if spec is None: # Likely passed a cfg file
|
|
parser = ConfigParser()
|
|
parser.optionxform = str # type: ignore # Prevents lowercasing of keys
|
|
with open(file_path) as file_:
|
|
config_str = '[section]\n' + file_.read()
|
|
parser.read_string(config_str)
|
|
self.from_mapping(parser['section'])
|
|
else:
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module) # type: ignore
|
|
self.from_object(module)
|
|
except (FileNotFoundError, IsADirectoryError):
|
|
if not silent:
|
|
raise
|
|
|
|
def from_object(self, instance: Union[object, str]) -> None:
|
|
"""Load the configuration from a Python object.
|
|
|
|
This can be used to reference modules or objects within
|
|
modules for example,
|
|
|
|
.. code-block:: python
|
|
|
|
app.config.from_object('module')
|
|
app.config.from_object('module.instance')
|
|
from module import instance
|
|
app.config.from_object(instance)
|
|
|
|
are valid.
|
|
|
|
Arguments:
|
|
instance: Either a str referencing a python object or the
|
|
object itself.
|
|
|
|
"""
|
|
if isinstance(instance, str):
|
|
try:
|
|
path, config = instance.rsplit('.', 1)
|
|
except ValueError:
|
|
path = instance
|
|
instance = importlib.import_module(path)
|
|
else:
|
|
module = importlib.import_module(path)
|
|
instance = getattr(module, config)
|
|
|
|
for key in dir(instance):
|
|
if key.isupper():
|
|
self[key] = getattr(instance, key)
|
|
|
|
def from_json(self, filename: str, silent: bool=False) -> None:
|
|
"""Load the configuration values from a JSON formatted file.
|
|
|
|
This allows configuration to be loaded as so
|
|
|
|
.. code-block:: python
|
|
|
|
app.config.from_json('config.json')
|
|
|
|
Arguments:
|
|
filename: The filename which when appended to
|
|
:attr:`root_path` gives the path to the file.
|
|
silent: If True any errors will fail silently.
|
|
"""
|
|
file_path = self.root_path / filename
|
|
try:
|
|
with open(file_path) as file_:
|
|
data = json.loads(file_.read())
|
|
except (FileNotFoundError, IsADirectoryError):
|
|
if not silent:
|
|
raise
|
|
else:
|
|
self.from_mapping(data)
|
|
|
|
def from_mapping(self, mapping: Optional[Mapping[str, Any]]=None, **kwargs: Any) -> None:
|
|
"""Load the configuration values from a mapping.
|
|
|
|
This allows either a mapping to be directly passed or as
|
|
keyword arguments, for example,
|
|
|
|
.. code-block:: python
|
|
|
|
config = {'FOO': 'bar'}
|
|
app.config.from_mapping(config)
|
|
app.config.form_mapping(FOO='bar')
|
|
|
|
Arguments:
|
|
mapping: Optionally a mapping object.
|
|
kwargs: Optionally a collection of keyword arguments to
|
|
form a mapping.
|
|
"""
|
|
mappings: Dict[str, Any] = {}
|
|
if mapping is not None:
|
|
mappings.update(mapping)
|
|
mappings.update(kwargs)
|
|
for key, value in mappings.items():
|
|
if key.isupper():
|
|
self[key] = value
|
|
|
|
def get_namespace(
|
|
self,
|
|
namespace: str,
|
|
lowercase: bool=True,
|
|
trim_namespace: bool=True,
|
|
) -> Dict[str, Any]:
|
|
"""Return a dictionary of keys within a namespace.
|
|
|
|
A namespace is considered to be a key prefix, for example the
|
|
keys ``FOO_A, FOO_BAR, FOO_B`` are all within the ``FOO``
|
|
namespace. This method would return a dictionary with these
|
|
keys and values present.
|
|
|
|
.. code-block:: python
|
|
|
|
config = {'FOO_A': 'a', 'FOO_BAR': 'bar', 'BAR': False}
|
|
app.config.from_mapping(config)
|
|
assert app.config.get_namespace('FOO_') == {'a': 'a', 'bar': 'bar'}
|
|
|
|
Arguments:
|
|
namespace: The namespace itself (should be uppercase).
|
|
lowercase: Lowercase the keys in the returned dictionary.
|
|
trim_namespace: Remove the namespace from the returned
|
|
keys.
|
|
"""
|
|
config = {}
|
|
for key, value in self.items():
|
|
if key.startswith(namespace):
|
|
if trim_namespace:
|
|
new_key = key[len(namespace):]
|
|
else:
|
|
new_key = key
|
|
if lowercase:
|
|
new_key = new_key.lower()
|
|
config[new_key] = value
|
|
return config
|