from pathlib import Path
from django.utils.text import slugify
from ...exceptions import ApplicationConfigError
from ..defaults import DEFAULT_FORMAT, AVAILABLE_FORMATS
from .store import get_appstore
appstore = get_appstore()
[docs]
class ApplicationConfig:
"""
Application model to validate and store application details.
.. todo:
Another name would better to avoid mental clash with Django "AppConfig".
ApplicationModel (+2) ? ApplicationDataDef (0)? ApplicationDefinition (+3) ?
DataDefinition (+3) ?
Arguments:
name (string): Application name, almost anything but it may be slugified for
internal usages so avoid too much longer text and special characters.
models (list): List of labels. A label is either an application label like
``auth`` or a full model label like ``auth.user``. Labels are validated,
they must exists in Django application registry.
Keyword Arguments:
filename (string): The filename to use if application dump is to be written in
a file. The filename also determine the format used to dump data. If you
want another format that the default one you will have to define
it from the filename even you don't plan to write dump to a file.
Finally if not given, the filename will be automatically defined with
slugified ``name`` with default format.
excludes (list): The list of excluded model labels that won't be collected
into the application dump. Currently, the excluded labels are not
validated.
natural_foreign (boolean): Enable usage of natural foreign key.
natural_primary (boolean): Enable usage of natural primary key.
comments (string): Free text not used internally.
allow_drain (boolean): Define if application allows its excluded models to be
drained. Default is ``False`` to avoid implicit draining of data that may
not be wanted.
dump_command (string): Custom dump command to use for this specific application
instead of default ``dumpdata``. If you have models that use
``django-polymorphic`` you should give value ``polymorphic_dumpdata`` here.
use_base_manager (boolean): Bypass possible custom manager from model(s). This
is the equivalent of ``all`` option from Django command ``dumpdata``.
Attributes:
CONFIG_ATTRS (list): List of object attribute names to export in application
configuration dump.
OPTIONS_ATTRS (list): List of object attribute names that will be passed to
Django ``dumpdata`` command. All attributes that are not in this list can
be assumed to be internal parameters.
is_drain (boolean): Declare application as a special drain application. This
should always be ``False`` for a common application, ``True`` value is
reserved to ``DrainApplicationConfig``.
"""
CONFIG_ATTRS = [
"name",
"models",
"excludes",
"retention",
"natural_foreign",
"natural_primary",
"comments",
"filename",
"is_drain",
"allow_drain",
"dump_command",
"use_base_manager",
]
OPTIONS_ATTRS = [
"models",
"excludes",
"natural_foreign",
"natural_primary",
"filename",
"use_base_manager",
]
def __init__(self, name, models=[], excludes=None, natural_foreign=False,
natural_primary=False, comments=None, filename=None,
is_drain=None, allow_drain=False, dump_command=None,
use_base_manager=False):
self.name = name
self._models = [models] if isinstance(models, str) else models
self._excludes = excludes or []
self.natural_foreign = natural_foreign
self.natural_primary = natural_primary
self.comments = comments
self.filename = filename or self.get_filename()
self.allow_drain = allow_drain
self.dump_command = dump_command
self.is_drain = False
self.use_base_manager = use_base_manager
def __repr__(self):
return "<{klass}: {name}>".format(
klass=self.__class__.__name__,
name=self.name
)
@property
def models(self):
"""
List all fully qualified model labels to include, either implicitely and
explicitely from given ``models`` argument.
NOTE: models, excludes and retention attributes may be cached since they have
no reason to change.
Returns:
list: Fully qualified model labels.
"""
return [
model.label
for model in appstore.get_models_inclusions(
self._models, excludes=self._excludes
)
]
@property
def excludes(self):
"""
List all fully qualified model labels to exclude, either implicitely and
explicitely from given ``_excludes`` argument.
Returns:
list: Fully qualified model labels.
"""
return [
model.label
for model in appstore.get_models_exclusions(
self._models, excludes=self._excludes
)
]
@property
def retention(self):
"""
List all fully qualified model labels that are not allowed to be drained from
this application.
Included models are never allowed to be drained and exclusions models may be
allowed if ``allow_drain`` is enabled.
Returns:
list: Fully qualified model labels that won't be allowed to be drained.
This means labels from ``models`` on defaut and possibly the
``excludes`` one also if application allows for drainage.
"""
if not self.allow_drain:
return self.models + self.excludes
return self.models
[docs]
def get_filename(self, format_extension=None):
"""
Automatically determine filename from Application name and with a format
extension name.
Keyword Arguments:
format_extension (string): Custom extension to use if given.
Returns
string: Filename.
"""
return slugify(self.name) + "." + (format_extension or DEFAULT_FORMAT)
[docs]
def as_config(self):
"""
Returns Application configuration suitable for dump history.
Keyword Arguments:
name (boolean): To include or not the name into the dict.
commented (boolean): To include or not the comments into the dict.
Returns
dict: Application data.
"""
return {
name: getattr(self, name)
for name in self.CONFIG_ATTRS
}
[docs]
def as_options(self):
"""
Returns Application options suitable to pass to dumpdata command.
Keyword Arguments:
name (boolean): To include or not the name into the dict.
commented (boolean): To include or not the comments into the dict.
Returns
dict: Application data.
"""
return {
name: getattr(self, name)
for name in self.OPTIONS_ATTRS
}
[docs]
def validate_includes(self):
"""
Validate include labels from ``_models`` attribute.
"""
# Models are required but not for an application drain object
if not self._models and self.is_drain is False:
msg = "{obj}: 'models' must not be an empty value."
raise ApplicationConfigError(msg.format(
obj=self.__repr__(),
))
unknow_apps, unknow_models = appstore.check_unexisting_labels(self._models)
if len(unknow_apps) > 0:
msg = (
"{obj}: Some given app labels to include does not exists: {labels}"
)
raise ApplicationConfigError(msg.format(
obj=self.__repr__(),
labels=", ".join(unknow_apps),
))
if len(unknow_models) > 0:
msg = (
"{obj}: Some given models labels to include does not exists: {labels}"
)
raise ApplicationConfigError(msg.format(
obj=self.__repr__(),
labels=", ".join(unknow_models),
))
[docs]
def validate_excludes(self):
"""
Validate exclude labels from ``excludes`` attribute.
"""
if not isinstance(self._excludes, list):
msg = "{obj}: 'excludes' argument must be a list."
raise ApplicationConfigError(msg.format(
obj=self.__repr__(),
))
else:
errors = appstore.is_fully_qualified_labels(self._excludes)
if errors:
msg = (
"{obj}: 'excludes' argument can only contains fully qualified "
"labels (like 'foo.bar') these ones are invalid: {labels}"
)
raise ApplicationConfigError(msg.format(
obj=self.__repr__(),
labels=", ".join(errors),
))
unknow_apps, unknow_models = appstore.check_unexisting_labels(self._excludes)
if len(unknow_apps) > 0:
msg = (
"{obj}: Some given app labels to exclude does not exists: {labels}"
)
raise ApplicationConfigError(msg.format(
obj=self.__repr__(),
labels=", ".join(unknow_apps),
))
if len(unknow_models) > 0:
msg = (
"{obj}: Some given models labels to exclude does not exists: {labels}"
)
raise ApplicationConfigError(msg.format(
obj=self.__repr__(),
labels=", ".join(unknow_models),
))
def validate_filename(self):
# Filename must have a file extension to discover serialization format
extension = Path(self.filename).suffix
if not extension:
msg = (
"{obj}: Given file name '{filename}' must have a file extension to "
"discover format."
)
raise ApplicationConfigError(msg.format(
obj=self.__repr__(),
filename=self.filename,
))
# File extension must correspond to an allowed format
else:
# Remove leading dot
extension = extension[1:]
if extension not in AVAILABLE_FORMATS:
msg = (
"{obj}: Given file name '{filename}' must use a file extension "
"from allowed formats: {formats}"
)
raise ApplicationConfigError(msg.format(
obj=self.__repr__(),
filename=self.filename,
formats=", ".join(AVAILABLE_FORMATS),
))
[docs]
def validate(self):
"""
Validate Application options.
Raises:
ApplicationConfigError: In case of invalid values from options.
"""
self.validate_filename()
self.validate_includes()
self.validate_excludes()
[docs]
class DrainApplicationConfig(ApplicationConfig):
"""
Special application to drain remaining models from apps.
On default a drain will dump anything that have not been defined from apps. Its
base goal is to dump data from undefined applications.
Attributes:
drain_excluded (boolean): If enabled, the drain will accept to drain exclusion
from applications which allow it. Else the drain will exclude also the
application exclusion. Default is disabled.
"""
CONFIG_ATTRS = [
"name",
"models",
"excludes",
"natural_foreign",
"natural_primary",
"use_base_manager",
"comments",
"filename",
"is_drain",
"drain_excluded",
"dump_command",
]
OPTIONS_ATTRS = [
"models",
"excludes",
"natural_foreign",
"natural_primary",
"use_base_manager",
"filename",
]
def __init__(self, *args, **kwargs):
self.drain_excluded = kwargs.pop("drain_excluded", False)
super().__init__(*args, **kwargs)
# Drain never allow for any inclusion
self._models = []
# Force as a drain only
self.is_drain = True
# It is not allowed to be drained itself
self.allow_drain = False
@property
def excludes(self):
"""
Just returns exclude labels as given since drain excludes are meaningful
enough.
Returns:
list: Fully qualified model labels.
"""
return self._excludes