AutoArchive._infrastructure.configuration.options — AutoArchive Developer Manual (ver. 2.0.6)

# Source code for AutoArchive._infrastructure.configuration.options

    # options.py
    #
    # Project: AutoArchive
    # License: GNU GPLv3
    #
    # Copyright (C) 2003 - 2021 Róbert Čerňanský

    """:class:`Options`, :class:`OptionsUtils` and :class:`SpecialOptionTypes` static classes. :class:`Option` class
    and :data:`ArchiverTypes` enum."""

    __all__ = ["ArchiverTypes", "Options", "OptionsUtils", "Option", "SpecialOptionTypes"]

    # {{{ INCLUDES

    from abc import ABCMeta, abstractmethod
    import os.path
    from AutoArchive._infrastructure.py_additions import Enum

    # }}} INCLUDES

    # {{{ CONSTANTS

    #: Archiver types.
    ArchiverTypes = Enum(
        "Tar",
        "TarGz",
        "TarBz2",
        "TarXz",
        "TarZst",
        "TarInternal",
        "TarGzInternal",
        "TarBz2Internal")

    # }}} CONSTANTS

    # {{{ CLASSES

    class Option:
        """Represents a configuration option.

        :param name: Option name.
        :type name: ``str``
        :param optType: Type of the option.
        :type optType: ``type`` or ``str``"""

        def __init__(self, name, optType):
            self.__name = name
            self.__optType = optType

        def __str__(self):
            return self.__name

        def __repr__(self):
            return str.format("<Option {} of type {}>", self.__name, self._optType)

        @property
        def _optType(self):
            """Gets the option type.

            :rtype: ``type`` or ``str``"""

            return self.__optType

    # all option type constants defined in this class has to have uppercase names; there can not be any other constants
    # defined that has uppercase names and are type of str, besides those representing option types

    class SpecialOptionTypes(metaclass = ABCMeta):
        """Constants for special option types.

        Normally, options are of some standard type, such as ``int``, ``str``, etc.  Some of them however, requires special
        handling for which the special option types are defined in this class.

        .. note:: It is not allowed to change values of these constants."""

        # {{{ special option types constants

        #: A filesystem path.
        PATH = "path"

        # }}} special option types constants

        @abstractmethod
        def __init__(self):
            pass

    # all option constants defined in this class has to have uppercase names; there can not be any other constants defined
    # that has uppercase names and are type of Option, besides those representing options

    class Options(metaclass = ABCMeta):
        """Constants for configuration options.

        These constants should be used to access options in the :class:`.IConfiguration` implementation provided by the
        :term:`Configuration` :term:`component`.

        .. note:: It is not allowed to change values of these constants."""

        # {{{ option constants

        # {{{ archiving related options

        #: Archiver type.  Guaranteed to be defined.
        ARCHIVER = Option("archiver", ArchiverTypes)

        #: Compression strength level.
        COMPRESSION_LEVEL = Option("compression-level", int)

        #: Directory where the backup will be created.  Guaranteed to be defined.
        DEST_DIR = Option("dest-dir", SpecialOptionTypes.PATH)

        #: If enabled, backups are overwritten at the start of creation.  If disabled (default), backups are overwritten
        #: at the end of creation.  Enabling this option can be useful with big backups and low free space on the backup
        #: volume.
        OVERWRITE_AT_START = Option("overwrite-at-start", bool)

        # }}} archiving related options

        # {{{ incremental archiving related options

        #: Incremental backup.
        INCREMENTAL = Option("incremental", bool)

        #: Backup level used in incremental archiving.
        LEVEL = Option("level", int)

        #: Turn on backup level restarting.
        RESTARTING = Option("restarting", bool)

        #: Maximal backup level.  If reached, it will be restarted back to a lower level (which is typically level 1 but it
        #: depends on :attr:`MAX_RESTART_LEVEL_SIZE`).  Guaranteed to be defined.
        RESTART_AFTER_LEVEL = Option("restart-after-level", int)

        #: Number of days after which the backup level is restarted.  Similarly to :attr:`RESTART_AFTER_LEVEL` it will be
        #: restarted to level 1 or higher.
        RESTART_AFTER_AGE = Option("restart-after-age", int)

        #: Number of backup level restarts after which the level is restarted to 0.
        FULL_RESTART_AFTER_COUNT = Option("full-restart-after-count", int)

        #: Number of days after which the backup level is restarted to 0.
        FULL_RESTART_AFTER_AGE = Option("full-restart-after-age", int)

        #: Maximal percentage size of a :term:`backup` (of level > 0) to which level is allowed restart to.  The size is
        #: percentage of size of the level 0 backup file.  If a backup of particular level has its size bigger than
        #: defined percentage, restart to that level will not be allowed.
        MAX_RESTART_LEVEL_SIZE = Option("max-restart-level-size", int)

        #: Turn on removing backups of levels that are no longer valid due to the backup level restart.  All backups of
        #: the backup level higher than the one currently being created will be removed.
        REMOVE_OBSOLETE_BACKUPS = Option("remove-obsolete-backups", bool)

        # }}} incremental archiving related options

        # {{{ options related to old backups keeping

        #: Turn on backup keeping.  When a backup is about to be overwritten, it is renamed instead.
        KEEP_OLD_BACKUPS = Option("keep-old-backups", bool)

        #: Number of old backups to keep when :attr:`KEEP_OLD_BACKUPS` is enabled.  Guaranteed to be defined.
        NUMBER_OF_OLD_BACKUPS = Option("number-of-old-backups", int)

        # }}} options related to old backups keeping

        # {{{ command execution options

        #: Arbitrary command that will be executed before backup creation for the set of selected archives.
        COMMAND_BEFORE_ALL_BACKUPS = Option("command-before-all-backups", str)

        #: Arbitrary command that will be executed after backup creation for the set of selected archives.
        COMMAND_AFTER_ALL_BACKUPS = Option("command-after-all-backups", str)

        #: Arbitrary command to execute prior to each backup creation.
        COMMAND_BEFORE_BACKUP = Option("command-before-backup", str)

        #: Arbitrary command to execute after each backup creation.
        COMMAND_AFTER_BACKUP = Option("command-after-backup", str)

        # }}} command execution options

        # {{{ general options

        #: Turn on verbose output.
        VERBOSE = Option("verbose", int)

        #: Turn on quiet output.  Only errors will be shown.  If QUIET is turned on at the same level as VERBOSE
        #: (e. g. both are specified on the command line) then QUIET has higher priority than VERBOSE.
        QUIET = Option("quiet", bool)

        #: Operate on all configured archive specification files.
        ALL = Option("all", bool)

        #: Directory where :term:`archive specification files <archive specification file>` will be searched.
        #: Guaranteed to be defined.
        ARCHIVE_SPECS_DIR = Option("archive-specs-dir", SpecialOptionTypes.PATH)

        #: User configuration file.  Guaranteed to be defined.
        USER_CONFIG_FILE = Option("user-config-file", SpecialOptionTypes.PATH)

        #: User configuration directory.  Guaranteed to be defined.
        USER_CONFIG_DIR = Option("user-config-dir", SpecialOptionTypes.PATH)

        # }}} general options

        # {{{ force options

        #: Force archiver type regardless to what is specified in the :term:`archive specification file`.
        FORCE_ARCHIVER = Option("force-archiver", ArchiverTypes)

        #: Force incremental backup regardless to what is specified in the :term:`archive specification file`.
        FORCE_INCREMENTAL = Option("force-incremental", bool)

        #: Force the backup level restarting regardless to what is specified in the :term:`archive specification file`.
        FORCE_RESTARTING = Option("force-restarting", bool)

        #: Force compression level regardless to what is specified in the :term:`archive specification file`.
        FORCE_COMPRESSION_LEVEL = Option("force-compression-level", int)

        #: Force the directory where the backup will be created.
        FORCE_DEST_DIR = Option("force-dest-dir", SpecialOptionTypes.PATH)

        #: Force configuration of the command to execute prior to each backup creation.
        FORCE_COMMAND_BEFORE_BACKUP = Option("force-command-before-backup", str)

        #: Force configuration of the command to execute after each backup creation.
        FORCE_COMMAND_AFTER_BACKUP = Option("force-command-after-backup", str)

        #: Force backup overwriting behavior regardless to what is specified in the :term:`archive specification file`.
        FORCE_OVERWRITE_AT_START = Option("force-overwrite-at-start", bool)

        # }}} force options

        # {{{ negation options

        #: Disable incremental backup.
        NO_INCREMENTAL = Option("no-incremental", bool)

        #: Turn off backup level restarting.
        NO_RESTARTING = Option("no-restarting", bool)

        #: Turn off obsolete backups removing.
        NO_REMOVE_OBSOLETE_BACKUPS = Option("no-remove-obsolete-backups", bool)

        #: Turn off backup keeping.
        NO_KEEP_OLD_BACKUPS = Option("no-keep-old-backups", bool)

        #: Do not operate on all configured archive specification files.
        NO_ALL = Option("no-all", bool)

        #: Do not overwrite backup at the start of creation.  Overwrite after the new backup is created.
        NO_OVERWRITE_AT_START = Option("no-overwrite-at-start", bool)

        # }}} negation options

        # }}} option constants

        @abstractmethod
        def __init__(self):
            pass

    class OptionsUtils(metaclass = ABCMeta):
        "Various utility methods working with :class:`Options`."

        __ARCHIVER_OPTION_ENUM_MAP = {"tar": ArchiverTypes.Tar,
                                      "targz": ArchiverTypes.TarGz,
                                      "tarbz2": ArchiverTypes.TarBz2,
                                      "tarxz": ArchiverTypes.TarXz,
                                      "tarzst": ArchiverTypes.TarZst,
                                      "tar_internal": ArchiverTypes.TarInternal,
                                      "targz_internal": ArchiverTypes.TarGzInternal,
                                      "tarbz2_internal": ArchiverTypes.TarBz2Internal}

        __NEGATION_PREFIX = "no-"
        __FORCE_PREFIX = "force-"

        @abstractmethod
        def __init__(self):
            pass

        @staticmethod
        def getAllOptions():
            """Iterator over all known options.

            :return: All options defined in :class:`Options`.
            :rtype: ``Iterator<Option>``"""

            for member in Options.__dict__:
                if member.isupper and isinstance(Options.__dict__[member], Option):
                    yield Options.__dict__[member]

        @staticmethod
        def getAllSpecialOptionTypes():
            """Iterator over all known special option types.

            :return: All option types defined in :class:`SpecialOptionTypes`.
            :rtype: ``Iterator<str>``"""

            for member in SpecialOptionTypes.__dict__:
                if member.isupper and isinstance(
                    SpecialOptionTypes.__dict__[member], str):

                    yield SpecialOptionTypes.__dict__[member]

        @classmethod
        def getOption(cls, optionName):
            """Return option with given name.

            :param optionName: Name of the option that shall be returned.
            :type optionName: ``str``

            :return: First option from :meth:`getAllOptions()` which name is ``optionName``.
            :rtype: :class:`Option`

            :raise KeyError: If option with name ``optionName`` does not exist."""

            try:
                return next(filter(lambda opt: str(opt) == optionName, cls.getAllOptions()))
            except StopIteration:
                raise KeyError(str.format("Unknown option with name: {}", optionName))

        @classmethod
        def isExistingOption(cls, optionName):
            """Check whether an option with name ``optionName`` does exists in :class:`OptionsUtils`.

            :param optionName: Name of the option which existence shall be checked.
            :type optionName: ``str``

            :return: ``True`` if option with name ``optionName`` exists; ``False`` otherwise.
            :rtype: ``bool``"""

            try:
                cls.getOption(optionName)
                return True
            except KeyError:
                return False

        @classmethod
        def tryGetNegationForm(cls, option):
            """Returns :term:`negation form <negation option form>` for ``option`` or ``None``.

            :param option: An option in the :term:`normal form <normal option form>` for which the negation form for shall
              be returned.
            :type option: :class:`Option`

            :return: Negation form of the passed ``option`` or ``None`` if it does not have a negation form.
            :rtype: :class:`Option`"""

            negationFormName = cls.__NEGATION_PREFIX + str(option)
            if cls.isExistingOption(negationFormName):
                return cls.getOption(negationFormName)
            return None

        @classmethod
        def tryGetForceForm(cls, option):
            """Returns :term:`force form <force option form>` for ``option`` or ``None``.

            :param option: An option in the :term:`normal form <normal option form>` for which the force form for shall be
              returned.
            :type option: :class:`Option`

            :return: Force form of the passed ``option`` or ``None`` if it does not have a force form.
            :rtype: :class:`Option`"""

            forceFormName = cls.__FORCE_PREFIX + str(option)
            if cls.isExistingOption(forceFormName):
                return cls.getOption(forceFormName)
            return None

        @classmethod
        def strToOptionType(cls, option, optionValue):
            """Converts string option value to its proper, defined type.

            :param option: Option which value shall be converted.
            :type option: :class:`Option`
            :param optionValue: Value to be converted.
            :type optionValue: ``str``

            :return: Converted ``optionValue``.
            :rtype: ``object``

            :raise ValueError: If ``optionValue`` can not be converted to ``option``\ 's type.
            :raise RuntimeError: If ``option``\ 's type is not supported."""

            result = None

            if optionValue is None:
                result = optionValue

            elif optionValue == "" and not issubclass(option._optType, str):
                result = None

            elif isinstance(option._optType, type):

                if issubclass(option._optType, str):
                    result = optionValue

                elif issubclass(option._optType, bool):
                    if optionValue.strip().lower() in ("false", "0", "no"):
                        result = False
                    else:
                        result = bool(optionValue)

                elif issubclass(option._optType, int):
                    result = int(optionValue)

                elif issubclass(option._optType, float):
                    result = float(optionValue)

            elif option._optType in cls.getAllSpecialOptionTypes():

                if option._optType == SpecialOptionTypes.PATH:
                    result = os.path.expanduser(optionValue)

            else:

                if option._optType is ArchiverTypes:
                    result = cls.__strToArchiverType(optionValue)

                else:
                    raise(RuntimeError("Unknown option type."))

            return result

        @classmethod
        def archiverTypeToStr(cls, archiverType):
            """Converts :data:`.ArchiverTypes` to string representation.

            Value of the ``archiverType`` parameter is converted to a string representation that is accepted by the
            :meth:`strToOptionType()` method.

            :param archiverType: Archiver type that shall be converted.
            :type archiverType: :data:`.ArchiverTypes`

            :return: String form of passed ``archiverType``.
            :rtype: ``str``

            :raise ValueError: If ``archiverType`` is not known."""

            try:
                return next(filter(
                        lambda key: cls.__ARCHIVER_OPTION_ENUM_MAP[key] ==
                        archiverType, cls.__ARCHIVER_OPTION_ENUM_MAP.keys()))
            except StopIteration:
                raise ValueError("Unknown archiver type: {}.", archiverType)

        @classmethod
        def __strToArchiverType(cls, archiverStr):
            "Converts string to :data:`.ArchiverTypes`."

            if archiverStr in cls.__ARCHIVER_OPTION_ENUM_MAP:
                return cls.__ARCHIVER_OPTION_ENUM_MAP[archiverStr]
            else:
                raise ValueError(str.format("Unknown archiver type: \"{}\".", archiverStr))

    # }}} CLASSES