class Config(object):
    """Global configuration object."""

    _logger = Logger.get_logger(__name__)

    @classmethod
    def __str__(cls):
        """Print class attributes in a controlled manner.

        This is intended to limit printing of sensitive configuration values
        by accident. This method should return only configuration values that
        can be printed without revealing configuration that is considered
        sensitive like:

        - User names and passwords.

        - SSL certificate paths and certificate names.

        - IP addresses, host names and service ports.

        - Full file paths. File paths are truncated to prevent revealing
          installation location in the server.

        - Database table names.

        - Internal database keys like the UUID primary key.

        - Direct printing of command line arguments.

        No sensitive information listed here must be printed. The data is not
        printed with any log level. The thought is that if sensitive data would
        be printed on any level, there would be accidental leak of information
        at some point. The server would be first run with highest debug setting
        and then left running with those.
        """

        namespace = []
        if not hasattr(cls, 'source'):
            return str('%s(%s)' % ('Config', ', '.join(namespace)))

        namespace.append('operation={}'.format(cls.operation))
        namespace.append('operation_uuid={}'.format(cls.operation_uuid))
        namespace.append('operation_digest={}'.format(cls.operation_digest))
        namespace.append('operation_identity={}'.format(cls.operation_identity))
        namespace.append('operation_filename={}'.format(cls.operation_filename))
        namespace.append('operation_file_format={}'.format(cls.operation_file_format))
        namespace.append('content_category={}'.format(cls.content_category))
        namespace.append('content_data={}'.format(cls.content_data))
        namespace.append('content_brief={}'.format(cls.content_brief.encode('utf-8')))
        namespace.append('content_description={}'.format(cls.content_description.encode('utf-8')))
        namespace.append('content_groups={}'.format(cls.content_groups))
        namespace.append('content_tags={}'.format(cls.content_tags))
        namespace.append('content_links={}'.format(cls.content_links))
        namespace.append('content_name={}'.format(cls.content_name.encode('utf-8')))
        namespace.append('content_filename={}'.format(cls.content_filename))
        namespace.append('content_versions={}'.format(cls.content_versions))
        namespace.append('content_languages={}'.format(cls.content_languages))
        namespace.append('content_source={}'.format(cls.content_source))
        namespace.append('plugin={}'.format(cls.plugin))
        namespace.append('run_healthcheck={}'.format(cls.run_healthcheck))
        namespace.append('search_all_kws={}'.format(cls.search_all_kws))
        namespace.append('search_cat_kws={}'.format(cls.search_cat_kws))
        namespace.append('search_tag_kws={}'.format(cls.search_tag_kws))
        namespace.append('search_grp_kws={}'.format(cls.search_grp_kws))
        namespace.append('search_filter={}'.format(cls.search_filter))
        namespace.append('search_limit={}'.format(cls.search_limit))
        namespace.append('search_offset={}'.format(cls.search_offset))
        namespace.append('sort_fields={}'.format(cls.sort_fields))
        namespace.append('storage_type={}'.format(cls.storage_type))
        namespace.append('storage_file=...{}'.format(os.sep.join(os.path.normpath(cls.storage_file).split(os.sep)[5:])))
        namespace.append('storage_schema=..{}'.format(os.sep.join(os.path.normpath(cls.storage_schema).split(os.sep)[5:])))
        namespace.append('server_host={}'.format(cls.server_host))
        namespace.append('server_base_path_rest={}'.format(cls.server_base_path_rest))
        namespace.append('server_minify_json={}'.format(cls.server_minify_json))
        namespace.append('server_readonly={}'.format(cls.server_readonly))
        namespace.append('editor={}'.format(cls.editor))
        namespace.append('use_ansi={}'.format(cls.use_ansi))

        return str('%s(%s)' % ('Config', ', '.join(namespace)))

    @classmethod
    def init(cls, args):
        """Initialize global configuration."""

        if args is None:
            args = []

        # Initialize colored CLI output for Windows.
        if Const.WINDOWS:
            from colorama import init  # pylint: disable=import-error, bad-option-value, import-outside-toplevel
            init()

        # Set logger and development configuration.
        cls._init_logs(args)

        cls.source = Cli(args)

        # Static storage and template configurations.
        cls.storage_type = cls.source.storage_type
        cls.storage_schema = cls._test_resource('data/storage', 'database.sql')
        cls.storage_path = cls.source.storage_path
        cls.storage_file = cls._storage_file()
        cls.storage_host = cls.source.storage_host
        cls.storage_user = cls.source.storage_user
        cls.storage_password = cls.source.storage_password
        cls.storage_database = cls.source.storage_database
        cls.storage_ssl_cert = cls.source.storage_ssl_cert
        cls.storage_ssl_key = cls.source.storage_ssl_key
        cls.storage_ssl_ca_cert = cls.source.storage_ssl_ca_cert
        cls.templates = {
            'text': {
                'snippet': cls._read_resource('data/templates', 'snippet.txt'),
                'solution': cls._read_resource('data/templates', 'solution.txt'),
                'reference': cls._read_resource('data/templates', 'reference.txt')
            },
            'mkdn': {
                'snippet': cls._read_resource('data/templates', 'snippet.md'),
                'solution': cls._read_resource('data/templates', 'solution.md'),
                'reference': cls._read_resource('data/templates', 'reference.md')
            }
        }
        cls.completion = {
            'bash': cls._read_resource('data/completion', 'snippy.bash-completion')
        }

        # Static server configurations.
        cls.server_readonly = cls.source.server_readonly
        cls.server_base_path_rest = cls.source.server_base_path_rest
        cls.server_minify_json = cls.source.server_minify_json
        cls.server_host = cls.source.server_host
        cls.server_ssl_cert = cls._ssl_file(cls.source.server_ssl_cert)
        cls.server_ssl_key = cls._ssl_file(cls.source.server_ssl_key)
        cls.server_ssl_ca_cert = cls._ssl_file(cls.source.server_ssl_ca_cert)

        # Server and healthchecks are started only once.
        cls.run_server = cls.source.run_server
        cls.run_healthcheck = cls.source.run_healthcheck

        # Dynamic configuration.
        cls.load(cls.source)

    @classmethod
    def load(cls, source):  # pylint: disable=too-many-statements
        """Load dynamic configuration from source."""

        cls.source = source

        # logger: Only quiet flag is updated. If all logging configuration
        # would be updated, the server would print logs only from the first
        # operation. The reason to update quiet flag is to be able to prevent
        # test cases to print unnecessary help dialog when test creates the
        # Snippy() object. This allows the first creation to be silent but
        # allows further configuration from tests with snippy.run().
        cls.quiet = bool(cls.source.quiet)
        cls._update_logger()
        cls._logger.debug('config source: %s', cls.source)

        # operation
        cls.operation = cls.source.operation
        cls.operation_digest = cls.source.digest
        cls.operation_uuid = cls.source.uuid
        cls.operation_identity = cls.source.identity
        cls.merge = cls.source.merge

        # content
        cls.content_category = cls.source.category
        cls.content_data = cls.source.data
        cls.content_brief = cls.source.brief
        cls.content_description = cls.source.description
        cls.content_name = cls.source.name
        cls.content_groups = cls.source.groups
        cls.content_tags = cls.source.tags
        cls.content_links = cls.source.links
        cls.content_source = cls.source.source
        cls.content_versions = cls.source.versions
        cls.content_languages = cls.source.languages
        cls.content_filename = cls.source.filename

        # search
        cls.search_cat_kws = cls.source.scat
        cls.search_all_kws = cls.source.sall
        cls.search_tag_kws = cls.source.stag
        cls.search_grp_kws = cls.source.sgrp
        cls.search_filter = cls.source.search_filter
        cls.search_limit = cls.source.search_limit
        cls.search_offset = cls.source.search_offset
        cls.remove_fields = cls.source.remove_fields
        cls.reset_fields = cls.source.reset_fields
        cls.sort_fields = cls.source.sort_fields

        # migrate
        cls.defaults = cls.source.defaults
        cls.template = cls.source.template
        cls.complete = cls.source.complete
        cls.plugin = cls.source.plugin

        # hooks
        #
        # There is a strange behaviour with Python 2 if the hook is assigned to
        # Config class attribute. This will fail calling the hook with 'unbound
        # method' error because the call expects Config class instance as first
        # parameter. This would work with Python 3. This is related to a change
        # between Python 2 and Python 3 [1]. Because of this, hooks are stored
        # into a dictionary.
        #
        # [1] https://stackoverflow.com/a/16244344
        cls.hooks = {}
        cls.hooks['import'] = cls.source.import_hook
        cls.import_hook = bool(cls.hooks['import'])

        # options
        cls.editor = cls.source.editor
        cls.template_format_used = cls.source.template_format_used
        cls.template_format = cls.source.template_format
        cls.use_ansi = not cls.source.no_ansi
        cls.only_headers = cls.source.only_headers
        cls.failure = cls.source.failure
        cls.failure_message = cls.source.failure_message

        # Parsed from defined configuration.
        cls.is_operation_create = bool(cls.operation == 'create')
        cls.is_operation_search = bool(cls.operation == 'search')
        cls.is_operation_unique = bool(cls.operation == 'unique')
        cls.is_operation_update = bool(cls.operation == 'update')
        cls.is_operation_delete = bool(cls.operation == 'delete')
        cls.is_operation_export = bool(cls.operation == 'export')
        cls.is_operation_import = bool(cls.operation == 'import')
        cls.is_category_snippet = bool(cls.content_category == Const.SNIPPET)
        cls.is_category_solution = bool(cls.content_category == Const.SOLUTION)
        cls.is_category_reference = bool(cls.content_category == Const.REFERENCE)
        cls.is_multi_category = bool(len(cls.search_cat_kws) > 1)
        cls.operation_filename = cls._operation_filename((cls.content_category,))
        cls.is_multi_filename = bool(len(cls.source.operation_files) > 1)
        cls.operation_file_format = cls._operation_file_format(cls.operation_filename)
        cls.is_operation_file_json = bool(cls.operation_file_format == Const.CONTENT_FORMAT_JSON)
        cls.is_operation_file_mkdn = bool(cls.operation_file_format == Const.CONTENT_FORMAT_MKDN)
        cls.is_operation_file_text = bool(cls.operation_file_format == Const.CONTENT_FORMAT_TEXT)
        cls.is_operation_file_yaml = bool(cls.operation_file_format == Const.CONTENT_FORMAT_YAML)

        cls.debug()

    @classmethod
    def reset(cls):
        """Reset configuration."""

        if cls.source.failure:
            cls._logger.debug('configuration failure: {}'.format(cls.source.failure_message))

        Profiler.disable()

    @classmethod
    def get_collection(cls, updates=None):
        """Get collection of resources.

        Read collection of resources from the used configuration source. If
        resource updates are provided on top of the configured content, the
        updates are merged or migrated to the configuration.

        Args:
            updates (obj): Updates for the configured content in a Resource().

        Returns:
            obj: Configured content in a Collection().
        """

        collection = Collection()
        timestamp = Config.utcnow()
        resource = cls._get_config(timestamp, collection, updates, merge=Config.merge)
        if cls.editor:
            template = resource.get_template(
                cls.content_category,
                cls.template_format,
                cls.templates
            )
            Editor.read(timestamp, cls.template_format, template, collection)
        else:
            collection.migrate(resource)

        return collection

    @classmethod
    def get_resource(cls, updates):
        """Get resource.

        Read a resource from the used configuration source. If an update is
        provided on top of configured content, the update is merged or
        migrated on top of configuration.

        Args:
            updates (obj): Resource() to be used on top of the configuration.

        Returns:
            obj: Updated Resource().
        """

        collection = cls.get_collection(updates)
        if len(collection) == 1:
            resource = next(collection.resources())
        else:
            cls._logger.debug('updating resource from configuration source failed: %d', len(collection))
            resource = None

        return resource

    @classmethod
    def _get_config(cls, timestamp, collection, updates, merge):
        """Read configured content fields.

        Read content fields read from one of the configuration sources. The
        configuration is migrated or merged to updates if they exist.

        Args:
            timestamp (str): IS8601 timestamp to be used with created collection.
            collection (obj): Collection() to store configured content.
            updates (obj): Resource() from existing content.
            merge (bool): Defines if content is merged or not.

        Returns:
            obj: Configured content with updates in Resource().
        """

        config = collection.get_resource(cls.content_category, timestamp)
        config.data = cls.content_data
        config.brief = cls.content_brief
        config.description = cls.content_description
        config.name = cls.content_name
        config.groups = cls.content_groups
        config.tags = cls.content_tags
        config.links = cls.content_links
        config.source = cls.content_source
        config.versions = cls.content_versions
        config.languages = cls.content_languages
        config.filename = cls.content_filename

        if updates:
            updates_ = updates.copy()
            if merge:
                for field in Config.reset_fields:
                    setattr(updates_, field, None)
                updates_.merge(config, validate=False)
            else:
                updates_.migrate(config, validate=False)
        else:
            updates_ = config
        updates_.seal(validate=False)

        return updates_

    @classmethod
    def _init_logs(cls, args):
        """Init logger and development configuration.

        Parse log configuration manually from sys.argv in order to initialize
        logger as early as possible. The same parameters are read eventually
        by the Cli class parser.

        Args:
            args (list): Command line arguments.
        """

        cls.debug_logs = Cli.read_arg('--debug', False, args)
        cls.log_json = Cli.read_arg('--log-json', False, args)
        cls.log_msg_max = Cli.read_arg('--log-msg-max', Logger.DEFAULT_LOG_MSG_MAX, args)
        cls.profiler = Cli.read_arg('--profile', False, args)
        cls.quiet = Cli.read_arg('-q', False, args)
        cls.very_verbose = Cli.read_arg('-vv', False, args)

        # Profile code.
        Profiler.enable(cls.profiler)

        cls._update_logger()

    @classmethod
    def _update_logger(cls):
        """Update logger configuration."""

        Logger.configure({
            'debug': cls.debug_logs,
            'log_json': cls.log_json,
            'log_msg_max': cls.log_msg_max,
            'quiet': cls.quiet,
            'very_verbose': cls.very_verbose
        })

        cls._logger.debug(
            'config log settings debug: {} :very verbose: {} :quiet: {} :json logs: {} :log msg max: {}'.format(
                cls.debug_logs,
                cls.very_verbose,
                cls.quiet,
                cls.log_json,
                cls.log_msg_max
            )
        )

    @classmethod
    def _read_resource(cls, path, filename):
        """Read Snippy resource file.

        Args:
            path (str): Relative path under the snippy project.
            filename (str): Resource filename.

        Returns:
            str: File read into a string.
        """

        resource_file = ''
        filename = cls._test_resource(path, filename)
        with io.open(filename, encoding='utf-8') as infile:
            resource_file = infile.read()

        return resource_file

    @staticmethod
    def _test_resource(path, filename):
        """Test if Snippy resource exists.

        Args:
            path (str): Relative path under the snippy project.
            filename (str): Resource filename.

        Returns:
            str: File if the resource exists.
        """

        filename = os.path.join(pkg_resources.resource_filename('snippy', path), filename)
        if not os.path.isfile(filename):
            Logger.print_status('NOK: cannot run because snippy resource file is not accessible: {}'.format(filename))
            sys.exit(1)

        return filename

    @classmethod
    def server_schema(cls):
        """Get REST API JSON validation schema.

        Returns:
            str: Server API schema to validate incoming HTTP requests.
        """

        request_resource = json.loads(cls._read_resource('data/server/openapi/schema', 'requestresource.json'))
        request_collection = json.loads(cls._read_resource('data/server/openapi/schema', 'requestcollection.json'))
        schema = {
            'oneOf': [request_resource, request_collection]
        }

        return schema

    @classmethod
    def content_schema(cls):
        """Get content attribute JSON schema.

        Returns:
            str: Content schema to validate attributes for clients.
        """

        request_attributes = json.loads(cls._read_resource('data/server/openapi/schema', 'pluginattributes.json'))
        schema = {
            'oneOf': [request_attributes]
        }

        return schema

    @classmethod
    def schema_base_uri(cls):
        """Get server API schema base URI.

        Returns:
            str: Path where the API schema is stored in URI format.
        """

        filepath = pkg_resources.resource_filename('snippy', 'data/server/openapi/schema/')
        if not os.path.isdir(filepath):
            Logger.print_status('NOK: cannot run because server api schema base uri is not accessible: {}'.format(filepath))
            sys.exit(1)

        return 'file:' + filepath

    @classmethod
    def _storage_file(cls):
        """Construct store file with absolute path.

        The in-memory database is supported only in Python 3. The Sqlite, which
        is used as a in-memory database, supports this feature only in Python 3.

        Returns:
            str: Path or URI to the storage.
        """

        if cls.source.run_healthcheck:
            return Const.EMPTY

        if Config.storage_type == Const.DB_IN_MEMORY and not Const.PYTHON2:
            return 'file::memory:?cache=shared'

        if Config.storage_path:
            storage_path = Config.storage_path
        else:
            storage_path = pkg_resources.resource_filename('snippy', 'data/storage')

        if os.path.exists(storage_path) and os.access(storage_path, os.W_OK):
            storage_file = os.path.join(storage_path, 'snippy.db')
        elif cls.source.server_readonly and os.path.exists(storage_path) and os.access(storage_path, os.R_OK):
            cls._logger.debug('running server in readonly mode')
            storage_file = os.path.join(storage_path, 'snippy.db')
        else:
            # This is a special case which prevents additional error log after
            # tool is already about to exit with help text from the CLI parser.
            if not cls.source.failure:
                Logger.print_status('NOK: cannot run because content storage path is not writable: {}'.format(storage_path))
            sys.exit(1)

        return storage_file

    @classmethod
    def _ssl_file(cls, filename):
        """Test that given SSL/TLS certificate or key file exist."""

        if filename is not None and not os.path.isfile(filename):
            Logger.print_status('NOK: cannot run secured server because ssl/tls certificate file cannot be read: {}'.format(filename))
            sys.exit(1)

        return filename

    @classmethod
    def get_operation_file(cls, collection=None):
        """Return filename for the operation.

        Deciding filename and format with export operation:

        1. The ``--file`` option always overrides any filename and format in
           case of export operation.

        2. When the ``--file`` option is not used, the ``--format`` option
           always overrides the file format in case of export operation.

        3. If ``--file`` or ``--format`` command line options are not used,
           the resource ``filename`` attribute will define the filename and
           format in case of export operation.

        4. If none of the above conditions are met, default values are used.

        If collection is provided with more than one resource, the operation
        file is still updated. The collection contain resources from different
        category than originally defined.

        Args:
            collection (obj): Resources in Collection() container.

        Returns:
            str: Operation filename.
        """

        filename = cls.operation_filename
        if cls.is_operation_export and collection and not cls.source.operation_files:
            if len(collection) == 1 and next(collection.resources()).filename:
                filename = next(collection.resources()).filename
                if cls.template_format_used:
                    filename_old = next(collection.resources()).filename
                    name, _ = os.path.splitext(filename_old)
                    filename = name + '.' + cls.template_format.lower()
                    cls._logger.debug('format option override the defined file format: %s :with: %s' % (filename_old, filename))
            else:
                categories = collection.category_list()
                filename = cls._operation_filename(categories)
            cls.operation_filename = filename
            cls.operation_file_format = cls._operation_file_format(filename)
            cls.is_operation_file_json = bool(cls.operation_file_format == Const.CONTENT_FORMAT_JSON)
            cls.is_operation_file_mkdn = bool(cls.operation_file_format == Const.CONTENT_FORMAT_MKDN)
            cls.is_operation_file_text = bool(cls.operation_file_format == Const.CONTENT_FORMAT_TEXT)
            cls.is_operation_file_yaml = bool(cls.operation_file_format == Const.CONTENT_FORMAT_YAML)

        return filename

    @classmethod
    def get_operation_files(cls):
        """Return operation files.

        If there is only one filename, it can be for example extracted from
        ``--defaults`` or ``--complete`` options. Only if there are multiple
        files, they are assumed to be a list of content files.

        The method filters out all files which file format is not supported.

        Returns:
            tuple: Operation filenames.
        """

        files = (cls.operation_filename,)
        if len(cls.source.operation_files) > 1:
            files = []
            for filename in cls.source.operation_files:
                if cls._operation_file_format(filename) != Const.CONTENT_FORMAT_NONE:
                    files.append(filename)

        return files

    @classmethod
    def get_plugin_uri(cls):
        """Return URI for the plugin.

        This method reads the ``--file`` option directly as is to be used
        with a plugin. The URI is always the first filename in the operation
        file list. I case of file globs, there can be multiple source files
        but this not supported with the plugin URI.

        Returns:
            str: URI to be used with the plugin.
        """

        uri = Const.EMPTY
        if cls.source.operation_files:
            uri = cls.source.operation_files[0]

        return uri

    @classmethod
    def is_supported_file_format(cls):
        """Test if file format is supported."""

        return bool(cls.is_operation_file_json or
                    cls.is_operation_file_mkdn or
                    cls.is_operation_file_text or
                    cls.is_operation_file_yaml)

    @classmethod
    def default_content_file(cls, category):
        """Return default content file.

        Args:
            category (str): User defined content category.

        Returns:
            str: Filename with absolute path.
        """

        filename = category + 's.yaml'
        filename = os.path.join(pkg_resources.resource_filename('snippy', 'data/defaults'), filename)

        return filename

    @classmethod
    def _operation_filename(cls, categories):
        """Return operation default filename

        Filename is set based on priority order of
          1) command line input
          2) content defaults operations, content template or shell completion
          3) content category specific defaults

        Args:
            categories (tuple): List of categories related to operation.

        Returns:
            str: Operation filename.
        """

        filename = None
        if len(cls.source.operation_files) == 1:
            filename = cls.source.operation_files[0]

        if cls.defaults:
            filename = cls.default_content_file(cls.content_category)

        if cls.template:
            filename = os.path.join('./', cls.content_category + '-template.' + cls.template_format.lower())

        if cls.complete and not filename:
            filename = os.path.join('./snippy.' + cls.complete + '-completion')

        if not filename:

            if len(categories) == 1:
                defaults = categories[0] + 's.' + cls.template_format
            else:
                defaults = 'content.' + cls.template_format
            filename = os.path.join('./', defaults)

        return filename

    @classmethod
    def _operation_file_format(cls, filename):
        """Extract operation file format.

        The file format must be exactly as defined for supported file formats.
        In case of shell completion, there is no file extension and the check
        passes.

        Args:
            filename (str): Filename with file extension defining the format.

        Returns:
            str: Operation file format.
        """

        file_format = Const.CONTENT_FORMAT_NONE
        name, extension = os.path.splitext(filename)
        if name and extension == '.json':
            file_format = Const.CONTENT_FORMAT_JSON
        elif name and extension in ('.md', '.mkdn'):
            file_format = Const.CONTENT_FORMAT_MKDN
        elif name and extension in ('.text', '.txt'):
            file_format = Const.CONTENT_FORMAT_TEXT
        elif name and extension in ('.yaml', '.yml'):
            file_format = Const.CONTENT_FORMAT_YAML
        elif not cls.complete and not cls.plugin:
            Cause.push(Cause.HTTP_BAD_REQUEST, 'cannot identify file format for file: {}'.format(filename))

        return file_format

    @classmethod
    def validate_search_context(cls, collection, operation):  # pylint: disable=too-many-branches
        """Validate content search context."""

        # Search keys are treated in priority order of 1) digest, 2) uuid, 3)
        # unknown identity, 4) content data and 5) search keywords. Search
        # keywords are already validated and invalid keywords are interpreted
        # as 'list all' which is always correct at this point.
        cls._logger.debug('validating search context with %d results', len(collection))
        if cls._is_content_digest():
            if cls.operation_digest:
                if not collection:
                    Cause.push(Cause.HTTP_NOT_FOUND,
                               'cannot find content with message digest: %s' % cls.operation_digest)
                elif len(collection) > 1:
                    Cause.push(Cause.HTTP_CONFLICT,
                               'content digest: %.16s :matched: %d :times preventing: %s :operation' %
                               (cls.operation_digest, len(collection), operation))
            else:
                Cause.push(Cause.HTTP_BAD_REQUEST, 'cannot use empty message digest for: %s :operation' % operation)
        elif cls._is_content_uuid():
            if cls.operation_uuid:
                if not collection:
                    Cause.push(Cause.HTTP_NOT_FOUND,
                               'cannot find content with content uuid: %s' % cls.operation_uuid)
                elif len(collection) > 1:
                    Cause.push(Cause.HTTP_CONFLICT,
                               'content uuid: %.16s :matched: %d :times preventing: %s :operation' %
                               (cls.operation_uuid, len(collection), operation))
            else:
                Cause.push(Cause.HTTP_BAD_REQUEST, 'cannot use empty content uuid for: %s :operation' % operation)
        elif cls._is_content_identity():
            if cls.operation_identity:
                if not collection:
                    Cause.push(Cause.HTTP_NOT_FOUND,
                               'cannot find content with content identity: %s' % cls.operation_identity)
                elif len(collection) > 1:
                    Cause.push(Cause.HTTP_CONFLICT,
                               'content identity: %.16s :matched: %d :times preventing: %s :operation' %
                               (cls.operation_identity, len(collection), operation))
            else:
                Cause.push(Cause.HTTP_BAD_REQUEST, 'cannot use empty content identity for: %s :operation' % operation)
        elif cls.content_data:
            if any(cls.content_data):
                data = Const.EMPTY.join(cls.content_data)
                data = data[:30] + (data[30:] and '...')
                if not collection:
                    Cause.push(Cause.HTTP_NOT_FOUND, 'cannot find content with content data: %s' % data)
                elif len(collection) > 1:
                    Cause.push(Cause.HTTP_CONFLICT,
                               'content data: %s :matched: %d :times preventing: %s :operation' %
                               (data, len(collection), operation))
            else:
                Cause.push(Cause.HTTP_BAD_REQUEST, 'cannot use empty content data for: %s :operation' % operation)
        elif cls._is_search_keywords():
            if not collection:
                Cause.push(Cause.HTTP_NOT_FOUND, 'cannot find content with given search criteria')
            elif len(collection) > 1:
                Cause.push(Cause.HTTP_CONFLICT,
                           'search keywords matched: %d :times preventing: %s :operation' % (len(collection), operation))
        else:
            Cause.push(Cause.HTTP_BAD_REQUEST, 'no message digest, content data or search keywords were provided')

    @classmethod
    def _is_search_keywords(cls):
        """Test if search is made with any of the search option.

        The seach categories (search_cat_kws) is not considered optional search
        keywords because this category is set always implicitly if not provided
        by the user.
        """

        return bool(cls.search_all_kws or cls.search_tag_kws or cls.search_grp_kws)

    @classmethod
    def _is_content_digest(cls):
        """Test if content digest was defined."""

        return bool(cls.operation_digest is not None)

    @classmethod
    def _is_content_uuid(cls):
        """Test if content uuid was defined."""

        return bool(cls.operation_uuid is not None)

    @classmethod
    def _is_content_identity(cls):
        """Test if content identity was defined."""

        return bool(cls.operation_identity is not None)

    @classmethod
    def is_search_criteria(cls):
        """Test if any of the search criterias were used."""

        criteria = False
        if cls._is_search_keywords()     \
           or cls._is_content_digest()   \
           or cls._is_content_uuid()     \
           or cls._is_content_identity() \
           or cls.content_data:
            criteria = True

        return criteria

    @staticmethod
    def utcnow():
        """Get UTC time stamp in ISO8601 format."""

        utc = datetime.datetime.utcnow()

        return utc.strftime('%Y-%m-%dT%H:%M:%S.%f+00:00')

    @classmethod
    def debug(cls):
        """Debug Config.

        Do not print any configuration attrubutes from here. Use only the
        string presentation of the Config class to print attributes. This
        is because of security reasons.
        """

        cls._logger.debug('config parsed: {}'.format(Config())) class Constants(object):  # pylint: disable=too-few-public-methods
    """Globals constants."""

    SPACE = ' '
    EMPTY = ''
    COMMA = ','
    NEWLINE = '\n'

    # Python 2 and 3 compatibility.
    PYTHON2 = sys.version_info.major == 2
    if PYTHON2:
        TEXT_TYPE = unicode  # noqa pylint: disable=undefined-variable
        BINARY_TYPE = str
    else:
        TEXT_TYPE = str
        BINARY_TYPE = bytes
    PYTHON37 = bool(sys.version_info >= (3, 7))

    # Windows
    WINDOWS = sys.platform == 'win32'  # This applies also to win64.

    # Content categories.
    CODE = 'code'
    SNIPPET = 'snippet'
    SOLUTION = 'solution'
    REFERENCE = 'reference'
    TODO = 'todo'
    ALL_CATEGORIES = 'all'
    UNKNOWN_CATEGORY = 'unknown'
    CATEGORIES = (SNIPPET, SOLUTION, REFERENCE)

    # Field categories.
    GROUPS = 'groups'
    TAGS = 'tags'
    FIELD_CATEGORIES = (GROUPS, TAGS)

    # Content delimiters to convert between string and tuple presentations.
    DELIMITER_DATA = NEWLINE
    DELIMITER_GROUPS = ','
    DELIMITER_LINKS = NEWLINE
    DELIMITER_TAGS = ','
    DELIMITER_VERSIONS = ','
    DELIMITER_LANGUAGES = ','

    # Separate snippet data from optional comment.
    SNIPPET_COMMENT = '  #  '
    SNIPPET_COMMENT_COLOR = '  \x1b[91m#\x1b[0m  '

    # Default values for content fields.
    DEFAULT_GROUPS = ('default',)

    # Content formats
    CONTENT_FORMAT_DICT = 'dict'
    CONTENT_FORMAT_JSON = 'json'
    CONTENT_FORMAT_MKDN = 'mkdn'
    CONTENT_FORMAT_TEXT = 'text'
    CONTENT_FORMAT_YAML = 'yaml'
    CONTENT_FORMAT_NONE = 'none'

    # Storage backends and databases.
    DB_SQLITE = 'sqlite'
    DB_POSTGRESQL = 'postgresql'
    DB_COCKROACHDB = 'cockroachdb'
    DB_IN_MEMORY = 'in-memory'
    STORAGES = (DB_SQLITE, DB_POSTGRESQL, DB_COCKROACHDB, DB_IN_MEMORY)

    # Regexp patterns.
    RE_MATCH_ANSI_ESCAPE_SEQUENCES = re.compile(r'''
        \x1b[^m]*m    # Match all ANSI escape sequences.
        ''', re.VERBOSE)

    RE_CATCH_COMMAND_AND_COMMENT = re.compile(r'''
        (?P<command>[\s\S]+?)     # Catch command untill following separator.
        (:?\s{1,}[#]{1}\s{1,}|$)  # Match optional separator between command and comment or end of the line.
        (?P<comment>[\s\S]+|$)    # Catch optional comment. This regexp forces empty string instead of None in optional group.
        ''', re.VERBOSE)

    RE_DO_NOT_MATCH_ANYTHING = re.compile(r'''
        \A(?!x)x  # Never match anything.
        ''', re.VERBOSE)

    RE_MATCH_NEWLINES = re.compile(u'''             # Unicode regexp compatible with Python 2 and 3.
        (:?[\\r\\n\\x0B\\x0C\u0085\u2028\u2029]+)   # Match all newlines inluding Unicode line break characters.
        ''', re.UNICODE | re.VERBOSE)               # https://stackoverflow.com/a/34936253

    RE_MATCH_MULTIPE_WHITESPACES = re.compile(r'''
        (:?[\s]{2,})  # Match multiple whitespaces.
        ''', re.VERBOSE) class Collection(object):  # pylint: disable=too-many-public-methods
    """Store list of contents in collection."""

    def __init__(self):
        self._logger = Logger.get_logger(__name__)
        self._data = self._init()

    def __str__(self):
        """Format string from the class object."""

        text = Const.EMPTY
        for i, resource in enumerate(self.resources(), start=1):
            text = text + resource.dump_term(index=i, only_headers=False, use_ansi=True, debug_logs=True)

        text = text + '\x1b[96;1m# \x1b[1;92mcollection meta\x1b[0m\n'
        text = text + '   \x1b[91m!\x1b[0m \x1b[2mtotal\x1b[0m : %d\n' % self._data['meta']['total']

        return text

    def __eq__(self, collection):
        """Compare collections if they are equal."""

        # See [1] and [2] for details how to override comparisons.
        #
        # [1] https://stackoverflow.com/a/30676267
        # [2] https://stackoverflow.com/a/390640
        if type(collection) is type(self):
            if len(self) != len(collection):
                return False
            for digest in self.keys():
                if not (digest in collection.keys() and self[digest] == collection[digest]):
                    return False

            return True

        return False

    def __ne__(self, collection):
        """Compare collections if they are not equal."""

        return not self == collection

    def __len__(self):
        """Return length of the collection."""

        return len(self._data['data'])

    def __iter__(self):
        """Return iterable resources from object."""

        return iter([self[digest] for digest in self.keys()])

    def __getitem__(self, digest):
        """Return item from object based on message digest."""

        return self._data['data'][digest]['data']

    def __delitem__(self, digest):
        """Return item from object based on message digest."""

        return self._data['data'][digest]['data']

    def keys(self):
        """Return list of digests in collection."""

        return list(self._data['data'].keys())

    def values(self):
        """Return list of resources in collection."""

        return list([self[digest] for digest in self.keys()])

    def resources(self):
        """Return generator for resources in collection."""

        for digest in self.keys():
            yield self[digest]

    @classmethod
    def get_resource(cls, category, timestamp):
        """Return new source."""

        return Resource(category, timestamp)

    def category_list(self):
        """Return list of categories in collection.

        Returns:
            tuple: List of unique categories in the collection.
        """

        categories = {}
        for resource in self.resources():
            categories[resource.category] = resource.category

        return tuple(categories.keys())

    def migrate(self, source):
        """Migrate resource or collection to collection.

        Add new resources or override originals if they exist.

        Args:
           source (content): Resource or Collection that is migrated.
        """

        migrated = False
        if isinstance(source, Collection):
            for resource in source.resources():
                self.migrate(resource)
        elif isinstance(source, Resource):
            if source.category in Const.CATEGORIES:
                if source.seal():
                    if source.digest not in self.keys():
                        self._data['meta']['total'] = self._data['meta']['total'] + 1
                    self._data['data'][source.digest] = {}
                    self._data['data'][source.digest]['data'] = source
                    migrated = True
                else:
                    self._logger.debug('resource: {} :not migrated to collection'.format(source.digest))
            else:
                self._logger.debug('migrate to collection failed due to unknown category: {}'.format(Logger.remove_ansi(str(source))))

        return migrated

    def merge(self, source):
        """Merge a resource to collection.

        Merge content into existing resource or create new content. Returns
        the message digest after merging the content.

        If content source does not exist in the collection it causes migrage
        operation.

        This method cannot merge a collection because so far there has been
        no need for it.

        Args:
            source (Resource): Resource to be merged to collection.

        Returns:
            str: None or message digest after merging the source.
        """

        digest = None
        if not source or not isinstance(source, Resource):
            self._logger.debug('source was not merged to collection: {}'.format(Logger.remove_ansi(str(source))))

            return digest

        if source.digest in self.keys():
            if self[source.digest].merge(source):
                digest = self[source.digest].digest
            else:
                self._logger.debug('merging content to existing resource failed: {}'.format(Logger.remove_ansi(str(source))))
                try:
                    del self[source.digest]
                except KeyError:
                    self._logger.info('unexpected failure to delete existing content: {}'.format(Logger.remove_ansi(str(source))))
        else:
            if self.migrate(source):
                digest = source.digest
            else:
                self._logger.debug('migrating new content to collection failed: {}'.format(Logger.remove_ansi(str(source))))
                try:
                    del self[source.digest]  # This should never be executed. But just in case of an error in above migrate method.
                except KeyError:
                    pass

        return digest

    def convert(self, resources, timestamp=''):
        """Convert resources into a Resource objects.

        Args:
            resource (list): List of resources
            timestamp (str): Optional IS8601 timestamp used with conversion.
        """

        for resource in resources:
            if isinstance(resource, (list, tuple)):
                resource = Resource(timestamp=timestamp, list_=resource)
            elif isinstance(resource, dict):
                resource = Resource(timestamp=timestamp, dict_=resource)
            self.migrate(resource)

    def load(self, content_format, timestamp, content):
        """Load content into collection.

        Args:
           content_format (Enum): Content format.
           timestamp (str): IS8601 timestamp used with created resources.
           content (str): Content to be read.
        """

        if content_format == Const.CONTENT_FORMAT_DICT:
            self.load_dict(timestamp, content)
        elif content_format == Const.CONTENT_FORMAT_MKDN:
            self.load_mkdn(timestamp, content)
        elif content_format == Const.CONTENT_FORMAT_TEXT:
            self.load_text(timestamp, content)

    def load_dict(self, timestamp, dictionary):
        """Convert dictionary to collection."""

        Parser(Const.CONTENT_FORMAT_DICT, timestamp, dictionary, self).read()

    def dump_dict(self, remove_fields=None):
        """Convert collection to dictionary.

        Args:
            remove_fields (list): List of content fields removed.
        """

        if remove_fields is None:
            remove_fields = []

        data = []
        for resource in self.resources():
            data.append(resource.dump_dict(remove_fields))

        return data

    def load_text(self, timestamp, text):
        """Load content from Markdown file.

        Args:
           text (str): Text formatted string.
        """

        Parser(Const.CONTENT_FORMAT_TEXT, timestamp, text, self).read()

    def dump_text(self, templates):
        """Convert collection to text format.

        All resources inside the collection are converted to text format.

        Args:
           templates (dict): Dictionary that contains content templates.

        Returns:
            string: Collection in text format.
        """

        text = Const.EMPTY
        if not self:
            Cause.push(Cause.HTTP_NOT_FOUND, 'cannot find content with given search criteria')
            return text

        for resource in self.resources():
            text = text + resource.dump_text(templates)
            text = text + Const.NEWLINE

        return text

    def load_mkdn(self, timestamp, mkdn):
        """Load content from Markdown file.

        Args:
           mkdn (str): Markdown formatted string.
        """

        Parser(Const.CONTENT_FORMAT_MKDN, timestamp, mkdn, self).read()

    def dump_mkdn(self, templates):
        """Convert collection to Markdown format.

        All resources inside the collection are converted to Markdown.

        Args:
           templates (dict): Dictionary that contains content templates.

        Returns:
            string: Collection in Markdown format.
        """

        text = Const.EMPTY
        if not self:
            Cause.push(Cause.HTTP_NOT_FOUND, 'cannot find content with given search criteria')
            return text

        for resource in self.resources():
            text = text + resource.dump_mkdn(templates)
            text = text + '\n---\n\n'
        text = text[:-6]  # Remove last separator added by the loop.

        return text

    def dump_term(self, templates, template_format, only_headers, use_ansi, debug_logs):
        """Convert collection for terminal.

        Args:
           templates (dict): Dictionary that contains content templates.
           template_format (str): Define the output format
           only_headers (bool): Define if only content headers are printed.
           use_ansi (bool): Define if ANSI characters are used.
           debug_logs (bool): Define if debut information is included.
        """

        text = Const.EMPTY
        if template_format == Const.CONTENT_FORMAT_MKDN:
            text = self.dump_mkdn(templates)
        else:
            text = self._dump_term(only_headers, use_ansi, debug_logs)

        self._logger.debug('printing content to terminal stdout')
        Logger.print_stdout(text)

    def _dump_term(self, only_headers, use_ansi, debug_logs):
        """Convert collection into terminal format.

        Args:
           only_headers (bool): Define if only content headers are printed.
           use_ansi (bool): Define if ANSI characters are used.
           debug_logs (bool): Define if debut information is included.

        Returns:
            string: Collection in text format.
        """

        text = Const.EMPTY
        if not self:
            Cause.push(Cause.HTTP_NOT_FOUND, 'cannot find content with given search criteria')
            return text

        for i, resource in enumerate(self.resources(), start=1):
            text = text + resource.dump_term(index=i, only_headers=only_headers, use_ansi=use_ansi, debug_logs=debug_logs)

        # Set one empty line at the end of string for beautified output.
        if self:
            text = text.rstrip()
            text = text + Const.NEWLINE

        return text

    @property
    def total(self):
        """Get total amount of resources without filters."""

        return self._data['meta']['total']

    @total.setter
    def total(self, value):
        """Total amount of resources without filters."""

        self._data['meta']['total'] = value

    def _init(self):
        """Wrap content list with metadata."""

        self._logger.debug('initialize new collection')
        meta_content = {
            'data': OrderedDict(),
            'meta': {
                'total': 0,  # Total amount of search results without filters.
            }
        }

        return meta_content class Database(object):
    """Helper methods for testing with generic database."""

    # Database options.
    DB_SQLITE = Helper.DB_SQLITE
    DB_POSTGRESQL = Helper.DB_POSTGRESQL
    DB_COCKROACHDB = Helper.DB_COCKROACHDB
    DB_IN_MEMORY = Helper.DB_IN_MEMORY
    _DATABASE = DB_SQLITE
    _DATABASES = (DB_SQLITE, DB_POSTGRESQL, DB_COCKROACHDB, DB_IN_MEMORY)
    _PLACEHOLDER = '?'

    # Mocked UUIDs must not collide with predefined resource UUIDs.
    TEST_UUIDS = (
        uuid.UUID(hex='a1cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='a2cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='a3cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='a4cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='a5cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='a6cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='a7cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='a8cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='a9cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='aacd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='abcd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='accd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='adcd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='aecd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='afcd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='b1cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='b2cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='b3cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='b4cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='b5cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='b6cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='b7cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='b8cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='b9cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='bacd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='bbcd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='bccd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='bdcd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='becd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='bfcd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='c1cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='c2cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='c3cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='c4cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='c5cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='c6cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='c7cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='c8cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='c9cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='cacd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='cbcd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='cccd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='cdcd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='cecd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='cfcd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='d1cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='d2cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='d3cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='d4cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='d5cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='d6cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='d7cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='d8cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='d9cd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='dacd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='dbcd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='dccd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='ddcd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='decd5827b6ef4067b5ac3ceac07dde9f'),
        uuid.UUID(hex='dfcd5827b6ef4067b5ac3ceac07dde9f')
    )
    TEST_UUIDS_STR = [str(uuid_) for uuid_ in TEST_UUIDS]
    UUID_EDIT = '99cd5827-b6ef-4067-b5ac-3ceac07dde9f'

    @classmethod
    def set_database(cls, database):
        """Set used database.

        Args:
            database (str): Database used in the tests.
        """

        if database not in cls._DATABASES:
            database = cls.DB_SQLITE
        cls._DATABASE = database

        if cls._DATABASE in (cls.DB_SQLITE, cls.DB_IN_MEMORY):
            cls._PLACEHOLDER = '?'
        else:
            cls._PLACEHOLDER = '%s'

        cls._assert_database_connection()

    @classmethod
    def get_database(cls):
        """Get used database.

        Returns:
            str: Database used in the tests.
        """

        return cls._DATABASE

    @staticmethod
    def get_count():
        """Get the total number of contents.

        Returns:
            int: Number of all rows in database.
        """

        total = 0
        try:
            connection = Database._connect()
            with closing(connection.cursor()) as cursor:
                cursor.execute('SELECT count(*) FROM contents')
                total = cursor.fetchone()
            connection.close()
        except (sqlite3.Error, psycopg2.Error) as error:
            print('database helper select exception: {}'.format(error))

        return total[0]

    @staticmethod
    def get_collection():
        """Return database rows as collection.

        This method may be called before the database is created. Because of
        this, the exception is silently discarded here.
        """

        rows = ()
        collection = Collection()
        try:
            connection = Database._connect()
            with closing(connection.cursor()) as cursor:
                cursor.execute('SELECT * FROM contents ORDER BY created ASC, brief ASC')
                rows = cursor.fetchall()
            connection.close()
            collection.convert(rows)
        except (sqlite3.Error, psycopg2.Error) as error:
            print('database helper select exception: {}'.format(error))

        return collection

    @classmethod
    def print_contents(cls):
        """Print database content."""

        print(cls.get_collection())

    @staticmethod
    def get_snippets():
        """Return snippets from database as collection."""

        return Database._select(Const.SNIPPET)

    @staticmethod
    def get_solutions():
        """Return solutions from database as collection."""

        return Database._select(Const.SOLUTION)

    @staticmethod
    def get_references():
        """Return references from database as collection."""

        return Database._select(Const.REFERENCE)

    @staticmethod
    def get_schema():
        """Return the file where the database schema is located."""

        schema = os.path.join(pkg_resources.resource_filename('snippy', 'data/storage'), 'database.sql')

        return  schema

    @staticmethod
    def get_schema_data():
        """Return the content of database schema file."""

        schema = (
            'CREATE TABLE IF NOT EXISTS contents',
            '          (',
            '                    id          UUID PRIMARY KEY NOT NULL UNIQUE',
            '                  , category    text DEFAULT "snippet"',
            '                  , data        text NOT NULL UNIQUE',
            '                  , brief       text DEFAULT ""',
            '                  , description text DEFAULT ""',
            '                  , name        text DEFAULT ""',
            '                  , groups      text DEFAULT ""',
            '                  , tags        text DEFAULT ""',
            '                  , links       text DEFAULT ""',
            '                  , source      text DEFAULT ""',
            '                  , versions    text DEFAULT ""',
            '                  , languages   text DEFAULT ""',
            '                  , filename    text DEFAULT ""',
            '                  , created     TIMESTAMP WITH TIME ZONE',
            '                  , updated     TIMESTAMP WITH TIME ZONE',
            '                  , uuid        UUID NOT NULL UNIQUE',
            '                  , digest      CHAR(64)',
            '          )',
            ';',
        )

        return Const.NEWLINE.join(schema)

    @staticmethod
    def get_storage():
        """Return the file where the database is located."""

        # Sqlite in Python2 does not support shared memory databases.
        # Because of this, the database is stored on disk.
        if not Const.PYTHON2:
            storage = 'file::memory:?cache=shared'
        else:
            storage = os.path.join(pkg_resources.resource_filename('snippy', 'data/storage'), 'snippy-test.db')

        return storage

    @classmethod
    def get_cli_params(cls):
        """Return CLI parameters for database."""

        params = []
        if cls._DATABASE == cls.DB_POSTGRESQL:
            params.append('--storage-type')
            params.append('postgresql')
            params.append('--storage-host')
            params.append('localhost:5432')
            params.append('--storage-database')
            params.append('postgres')
            params.append('--storage-user')
            params.append('postgres')
            params.append('--storage-password')
            params.append('')

        return params

    @classmethod
    def store(cls, content):
        """Store content into database.

        Args:
            content (dict): Content in a dictionary.
        """

        query = '''
            INSERT INTO contents
                      (
                                id
                              , category
                              , data
                              , brief
                              , description
                              , name
                              , groups
                              , tags
                              , links
                              , source
                              , versions
                              , languages
                              , filename
                              , created
                              , updated
                              , uuid
                              , digest
                      )
                      VALUES
                      (
                              {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}
                      )
        '''.format(cls._PLACEHOLDER)
        qargs = (
            content.get('id', Database.TEST_UUIDS_STR[0]),
            content.get('category', ''),
            Const.DELIMITER_DATA.join(map(Const.TEXT_TYPE, content.get('data', ()))),
            content.get('brief', ''),
            content.get('description', ''),
            content.get('name', ''),
            Const.DELIMITER_GROUPS.join(map(Const.TEXT_TYPE, sorted(content.get('groups', ('default',))))),
            Const.DELIMITER_TAGS.join(map(Const.TEXT_TYPE, sorted(content.get('tags', ())))),
            Const.DELIMITER_LINKS.join(map(Const.TEXT_TYPE, content.get('links', ()))),
            content.get('source', ''),
            Const.DELIMITER_VERSIONS.join(map(Const.TEXT_TYPE, content.get('versions', ()))),
            Const.DELIMITER_LANGUAGES.join(map(Const.TEXT_TYPE, content.get('languages', ()))),
            content.get('filename', ''),
            content.get('created', Helper.IMPORT_TIME),
            content.get('updated', Helper.IMPORT_TIME),
            content.get('uuid', Database.TEST_UUIDS_STR[0]),
            content.get('digest', '')
        )
        try:
            connection = Database._connect()
            with closing(connection.cursor()) as cursor:
                cursor.execute(query, qargs)
                connection.commit()
        except (sqlite3.Error, psycopg2.Error):
            print(traceback.format_exc())

    @staticmethod
    def delete_all_contents():
        """Delete all content from database."""

        # In successful case the database table does not exist anymore
        # and exception is allowed.
        try:
            connection = Database._connect()
            with closing(connection.cursor()) as cursor:
                cursor.execute('DELETE FROM contents')
                connection.commit()
            connection.close()
        except (sqlite3.Error, psycopg2.Error):
            pass

    @staticmethod
    def delete_storage():
        """Delete the database file created for the test."""

        # The file based database is used in testing only in case of Python2.
        if Const.PYTHON2:
            filename = Database.get_storage()
            if os.path.isfile(filename):
                try:
                    os.remove(filename)
                except OSError:
                    pass

    @classmethod
    def _connect(cls):
        """Connect to database."""

        if cls._DATABASE in (cls.DB_SQLITE, cls.DB_IN_MEMORY):
            if not Const.PYTHON2:
                connection = sqlite3.connect(Database.get_storage(), check_same_thread=False, uri=True)  # pylint: disable=unexpected-keyword-arg
            else:
                connection = sqlite3.connect(Database.get_storage(), check_same_thread=False)
        elif cls._DATABASE == cls.DB_POSTGRESQL:
            connection = psycopg2.connect(host='localhost', user='postgres', password='')

        return connection

    @classmethod
    def _assert_database_connection(cls):
        """Test that database can be connencted."""

        try:
            cls._connect()
        except (sqlite3.Error, psycopg2.Error):
            pytest.exit('cannot connect to used database: {}'.format(cls._DATABASE))

    @classmethod
    def _select(cls, category):
        """Return content based on category."""

        rows = ()
        collection = Collection()
        try:
            query = ('SELECT * FROM contents WHERE category={0} ORDER BY created ASC, brief ASC'.format(cls._PLACEHOLDER))
            qargs = [category]
            connection = Database._connect()
            with closing(connection.cursor()) as cursor:
                cursor.execute(query, qargs)
                rows = cursor.fetchall()
            connection.close()
        except (sqlite3.Error, psycopg2.Error):
            print(traceback.format_exc())

        collection.convert(rows)

        return collection class Helper(object):
    """Generic helpers testing.

    This class intentionally copies some of the implementation from the
    production code. The purpose is to avoid dependencies in this module
    to be able to import this module anywhere.
    """

    EXPORT_TIME = '2018-02-02T02:02:02.000001+00:00'
    IMPORT_TIME = '2018-03-02T02:02:02.000001+00:00'
    EXPORT_TEMPLATE = '2017-10-14T19:56:31.000001+00:00'

    DB_SQLITE = 'sqlite'
    DB_POSTGRESQL = 'postgresql'
    DB_COCKROACHDB = 'cockroachdb'
    DB_IN_MEMORY = 'in-memory'
    STORAGES = (DB_SQLITE, DB_POSTGRESQL, DB_COCKROACHDB, DB_IN_MEMORY)

    COLOR_OK = '\033[32m'
    COLOR_END = '\033[0m'

    # All resource attributes that can be sent in HTTP request.
    REQUEST_ATTRIBUTES = (
        'data',
        'brief',
        'description',
        'name',
        'groups',
        'tags',
        'links',
        'source',
        'versions',
        'languages',
        'filename'
    )

    RE_MATCH_ANSI_ESCAPE_SEQUENCES = re.compile(r'''
        \x1b[^m]*m    # Match all ANSI escape sequences.
        ''', re.VERBOSE)

    RE_MATCH_LEADING_WHITEPSACES = re.compile(r'''
        \n\s+   # Match newline and all leading whitespaces after it.
        ''', re.VERBOSE)

    @classmethod
    def read_template(cls, filename):
        """Get default content template in text format.

        The returned template must be in the same format where external editor
        like vi gets the default template. This means that all the tags are
        removed and the group tag is replaced with 'default' group.

        Args:
            filename (str): Template filename as stored in data/templates.

        Returns:
            str: Empty template in the same format as for external editor.
        """

        template = cls._read_resource('data/templates', filename)
        template = re.sub(r'''
            <groups>    # Match groups tag.
            ''', 'default', template, flags=re.VERBOSE)

        template = re.sub(r'''
            [<]\S+[>]   # Match any tag in the template.
            ''', '', template, flags=re.VERBOSE)

        # In case of the solution template, there is a <data> tag that leaves
        # empty fist line. Since all templates start from the first line, the
        # whitespaces can be removed from left of the string.
        template = template.lstrip()

        return template

    @classmethod
    def read_completion(cls, filename):
        """Get shell completion script.

        Args
            filename (str): Name of the shell completion file.
        """

        return cls._read_resource('data/completion', filename)

    @staticmethod
    def remove_ansi(message):
        """Remove all ANSI escape codes from given string.

        Args:
            message (str): Message which ANSI escape codes are removed.

        Returns:
            str: Same message but without ANSI escape sequences.
        """

        return Helper.RE_MATCH_ANSI_ESCAPE_SEQUENCES.sub('', message)

    @classmethod
    def get_schema_validator(cls):
        """Get JSON schema validator for REST API response.

        Returns:
            obj: Jsonschema draft7 validator.
        """

        response_resource = json.loads(cls._read_resource('data/server/openapi/schema', 'responseresource.json'))
        response_collection_get = json.loads(cls._read_resource('data/server/openapi/schema', 'responsecollectionget.json'))
        response_collection_post = json.loads(cls._read_resource('data/server/openapi/schema', 'responsecollectionpost.json'))
        response_groups = json.loads(cls._read_resource('data/server/openapi/schema', 'responsegroups.json'))
        response_tags = json.loads(cls._read_resource('data/server/openapi/schema', 'responsetags.json'))
        response_errors = json.loads(cls._read_resource('data/server/openapi/schema', 'responseerrors.json'))
        response_hello = json.loads(cls._read_resource('data/server/openapi/schema', 'responsehello.json'))
        schema = {
            'oneOf': [
                response_collection_get,
                response_collection_post,
                response_groups,
                response_tags,
                response_errors,
                response_hello,
                response_resource
            ]
        }

        filepath = pkg_resources.resource_filename('snippy', 'data/server/openapi/schema/')
        if not os.path.isdir(filepath):
            print('NOK: cannot run test because server api response schema base uri is not accessible: {}'.format(filepath))
            sys.exit(1)
        server_schema_base_uri = 'file:' + filepath
        Draft7Validator.check_schema(schema)
        resolver = RefResolver(base_uri=server_schema_base_uri, referrer=schema)
        validator = Draft7Validator(schema, resolver=resolver, format_checker=None)

        return validator

    @staticmethod
    def _read_resource(path, filename):
        """Read resource file.

        Args:
            path (str): Relative path under snippy project.
            filename (str): Resource filename.

        Returns:
            str: File read into a string.
        """

        filename = os.path.join(pkg_resources.resource_filename('snippy', path), filename)
        if not os.path.isfile(filename):
            print('NOK: cannot run tests because snippy resource file is not accessible: {}'.format(filename))
            sys.exit(1)

        resource_file = ''
        with io.open(filename, encoding='utf-8') as infile:
            resource_file = infile.read()

        return resource_file class Classproperty(object):  # pylint: disable=too-few-public-methods
    """Implement classproperty.

    Implement decorator that mimics object property. See [1] for more
    details.

    [1] https://stackoverflow.com/a/3203659
    """

    def __init__(self, getter):
        self._getter = getter

    def __get__(self, _, owner):
        """Get property of a class."""

        return self._getter(owner) class Reference(object):  # pylint: disable=too-few-public-methods
    """Default references for testing."""

    _GITLOG = 0
    _REGEXP = 1
    _PYTEST = 2

    # Default time is same for the default content. See 'Test case layouts and
    # data structures' for more information.
    DEFAULT_TIME = '2018-06-22T13:11:13.678729+00:00'

    # Default content must be always set so that it reflects content stored
    # into database. For example the tags must be sorted in correct order.
    # This forces defining erroneous content in each test case. This improves
    # the readability and maintainability of failure testing.
    _DEFAULTS = ({
        'category': 'reference',
        'data': (),
        'brief': 'How to write commit messages',
        'description': '',
        'name': '',
        'groups': ('git',),
        'tags': ('commit', 'git', 'howto'),
        'links': ('https://chris.beams.io/posts/git-commit/', ),
        'source': '',
        'versions': (),
        'languages': (),
        'filename': '',
        'created': DEFAULT_TIME,
        'updated': DEFAULT_TIME,
        'uuid': '31cd5827-b6ef-4067-b5ac-3ceac07dde9f',
        'digest': '5c2071094dbfaa33787064a6669e1fdfe49a86d07e58f12fffa0780eecdb227f'
    }, {
        'category': 'reference',
        'data': (),
        'brief': 'Python regular expression',
        'description': '',
        'name': '',
        'groups': ('python',),
        'tags': ('howto', 'online', 'python', 'regexp'),
        'links': ('https://www.cheatography.com/davechild/cheat-sheets/regular-expressions/',
                  'https://pythex.org/'),
        'source': '',
        'versions': (),
        'languages': (),
        'filename': '',
        'created': DEFAULT_TIME,
        'updated': DEFAULT_TIME,
        'uuid': '32cd5827-b6ef-4067-b5ac-3ceac07dde9f',
        'digest': 'cb9225a81eab8ced090649f795001509b85161246b46de7d12ab207698373832'
    }, {
        'category': 'reference',
        'data': (),
        'brief': 'Python pytest framework',
        'description': '',
        'name': '',
        'groups': ('python',),
        'tags': ('docs', 'pytest', 'python'),
        'links': ('https://docs.pytest.org/en/latest/skipping.html', ),
        'source': '',
        'versions': (),
        'languages': (),
        'filename': '',
        'created': '2016-04-21T12:10:11.678729+00:00',
        'updated': '2016-04-21T12:10:11.678729+00:00',
        'uuid': '33cd5827-b6ef-4067-b5ac-3ceac07dde9f',
        'digest': '1f9d9496005736efe321d44a28c05ca9ed0e53f7170743df361ddcd7b884455e'
    })

    GITLOG_CREATED = _DEFAULTS[_GITLOG]['created']
    GITLOG_UPDATED = _DEFAULTS[_GITLOG]['updated']
    REGEXP_CREATED = _DEFAULTS[_REGEXP]['created']
    REGEXP_UPDATED = _DEFAULTS[_REGEXP]['updated']
    PYTEST_CREATED = _DEFAULTS[_PYTEST]['created']
    PYTEST_UPDATED = _DEFAULTS[_PYTEST]['updated']

    if not DEFAULT_TIME == GITLOG_CREATED == GITLOG_UPDATED == REGEXP_CREATED == REGEXP_UPDATED:
        raise Exception('default content timestamps must be same - see \'Test case layouts and data structures\'')

    GITLOG_DIGEST = _DEFAULTS[_GITLOG]['digest']
    REGEXP_DIGEST = _DEFAULTS[_REGEXP]['digest']
    PYTEST_DIGEST = _DEFAULTS[_PYTEST]['digest']
    GITLOG_UUID = _DEFAULTS[_GITLOG]['uuid']
    REGEXP_UUID = _DEFAULTS[_REGEXP]['uuid']
    PYTEST_UUID = _DEFAULTS[_PYTEST]['uuid']

    GITLOG = _DEFAULTS[_GITLOG]
    REGEXP = _DEFAULTS[_REGEXP]
    PYTEST = _DEFAULTS[_PYTEST]
    DEFAULT_REFERENCES = (GITLOG, REGEXP)

    TEMPLATE = Helper.read_template('reference.txt').split('\n')
    TEMPLATE_DIGEST_EMPTY = 'bb4c2540fab3a12b051b77b6902f426812ec95f8a1fa9e07ca1b7dc3cca0cc0d'
    TEMPLATE_TEXT = (
        '# Commented lines will be ignored.',
        '#',
        '# Add mandatory links below one link per line.',
        '',
        '',
        '# Add optional brief description below.',
        'Add brief title for content',
        '',
        '# Add optional description below.',
        'Add a description that defines the content in one chapter.',
        '',
        '# Add optional name below.',
        'example content handle',
        '',
        '# Add optional comma separated list of groups below.',
        'groups',
        '',
        '# Add optional comma separated list of tags below.',
        'example,tags',
        '',
        '# Add optional source reference below.',
        'https://www.example.com/source.md',
        '',
        '# Add optional comma separated list of key-value versions below.',
        'example=3.9.0,python>=3',
        '',
        '# Add optional comma separated list of languages below.',
        'example-language',
        '',
        '# Add optional filename below.',
        'example-content.md',
        '',
        '# Meta',
        'category : reference',
        'created  : 2018-02-02T02:02:02.000001+00:00',
        'digest   : f5693f66ec42e8f42687e8639df806e79aead1dad1a6c1b5448b6220995518bd',
        'updated  : 2018-02-02T02:02:02.000001+00:00',
        'uuid     : a1cd5827-b6ef-4067-b5ac-3ceac07dde9f',
        ''
    )
    TEMPLATE_MKDN = (
        '# Add brief title for content @groups',
        '',
        '> Add a description that defines the content in one chapter.',
        '',
        '> [1] https://www.example.com/add-links-here.html',
        '',
        '## Meta',
        '',
        '> category  : reference  ',
        'created   : 2018-02-02T02:02:02.000001+00:00  ',
        'digest    : bb4c2540fab3a12b051b77b6902f426812ec95f8a1fa9e07ca1b7dc3cca0cc0d  ',
        'filename  : example-content.md  ',
        'languages : example-language  ',
        'name      : example content handle  ',
        'source    : https://www.example.com/source.md  ',
        'tags      : example,tags  ',
        'updated   : 2018-02-02T02:02:02.000001+00:00  ',
        'uuid      : a1cd5827-b6ef-4067-b5ac-3ceac07dde9f  ',
        'versions  : example=3.9.0,python>=3  ',
        ''
    ) class Snippet(object):  # pylint: disable=too-few-public-methods
    """Default snippets for testing."""

    _REMOVE = 0
    _FORCED = 1
    _EXITED = 2
    _NETCAT = 3
    _UMOUNT = 4
    _INTERP = 5

    # Default time is same for the default content. See 'Test case layouts and
    # data structures' for more information.
    DEFAULT_TIME = '2017-10-14T19:56:31.000001+00:00'

    # Default content must be always set so that it reflects content stored
    # into database. For example the tags must be sorted in correct order.
    # This forces defining erroneous content in each test case. This improves
    # the readability and maintainability of failure testing.
    _DEFAULTS = ({
        'category': 'snippet',
        'data': ('docker rm --volumes $(docker ps --all --quiet)',),
        'brief': 'Remove all docker containers with volumes',
        'description': '',
        'name': '',
        'groups': ('docker',),
        'tags': ('cleanup', 'container', 'docker', 'docker-ce', 'moby'),
        'links': ('https://docs.docker.com/engine/reference/commandline/rm/', ),
        'source': '',
        'versions': (),
        'languages': (),
        'filename': '',
        'created': DEFAULT_TIME,
        'updated': DEFAULT_TIME,
        'uuid': '11cd5827-b6ef-4067-b5ac-3ceac07dde9f',
        'digest': '54e41e9b52a02b631b5c65a6a053fcbabc77ccd42b02c64fdfbc76efdb18e319'
    }, {
        'category': 'snippet',
        'data': ('docker rm --force redis', ),
        'brief': 'Remove docker image with force',
        'description': '',
        'name': '',
        'groups': ('docker',),
        'tags': ('cleanup', 'container', 'docker', 'docker-ce', 'moby'),
        'links': ('https://docs.docker.com/engine/reference/commandline/rm/',
                  'https://www.digitalocean.com/community/tutorials/how-to-remove-docker-' +
                  'images-containers-and-volumes'),
        'source': '',
        'versions': (),
        'languages': (),
        'filename': '',
        'uuid': '12cd5827-b6ef-4067-b5ac-3ceac07dde9f',
        'created': DEFAULT_TIME,
        'updated': DEFAULT_TIME,
        'digest': '53908d68425c61dc310c9ce49d530bd858c5be197990491ca20dbe888e6deac5'
    }, {
        'category': 'snippet',
        'data': ('docker rm $(docker ps --all -q -f status=exited)',
                 'docker images -q --filter dangling=true | xargs docker rmi'),
        'brief': 'Remove all exited containers and dangling images',
        'description': '',
        'name': '',
        'groups': ('docker',),
        'tags': ('cleanup', 'container', 'docker', 'docker-ce', 'image', 'moby'),
        'links': ('https://docs.docker.com/engine/reference/commandline/images/',
                  'https://docs.docker.com/engine/reference/commandline/rm/',
                  'https://docs.docker.com/engine/reference/commandline/rmi/'),
        'source': '',
        'versions': (),
        'languages': (),
        'filename': '',
        'created': '2017-10-20T07:08:45.000001+00:00',
        'updated': '2017-10-20T07:08:45.000001+00:00',
        'uuid': '13cd5827-b6ef-4067-b5ac-3ceac07dde9f',
        'digest': '49d6916b6711f13d67960905c4698236d8a66b38922b04753b99d42a310bcf73'
    }, {
        'category': 'snippet',
        'data': ('nc -v 10.183.19.189 443',
                 'nmap 10.183.19.189'),
        'brief': 'Test if specific port is open',
        'description': '',
        'name': '',
        'groups': ('linux',),
        'tags': ('linux', 'netcat', 'networking', 'port'),
        'links': ('https://www.commandlinux.com/man-page/man1/nc.1.html',),
        'source': '',
        'versions': (),
        'languages': (),
        'filename': '',
        'created': '2017-10-20T07:08:45.000001+00:00',
        'updated': '2017-10-20T07:08:45.000001+00:00',
        'uuid': '14cd5827-b6ef-4067-b5ac-3ceac07dde9f',
        'digest': 'f3fd167c64b6f97e5dab4a3aebef678ef7361ba8c4a5acbc1d3faff968d4402d'
    }, {
        'category': 'snippet',
        'data': ('lsof | grep \'/tmp/overlayfs/overlay\'',
                 'kill <pid>',
                 'umount /tmp/overlayfs/overlay'),
        'brief': 'Umount a busy device',
        'description': '',
        'name': '',
        'groups': ('linux',),
        'tags': ('device', 'linux', 'umount'),
        'links': ('https://stackoverflow.com/a/7878763',),
        'source': '',
        'versions': (),
        'languages': (),
        'filename': '',
        'created': '2018-05-07T11:11:55.000001+00:00',
        'updated': '2018-05-07T11:11:55.000001+00:00',
        'uuid': '15cd5827-b6ef-4067-b5ac-3ceac07dde9f',
        'digest': '490c913cf941a0bedc14e3d390894958b3db4220dc2b1b856454403c888df17f'
    }, {
        'category': 'snippet',
        'data': ('find . -type d -name \'.git\' | while read dir ; do sh -c "cd $dir/../ && echo -e \\"\\nGIT STATUS IN ${dir//\\.git/}\\" && git status -s" ; done',),  # pylint: disable=line-too-long
        'brief': 'Perform recursive git status on subdirectories',
        'description': '',
        'name': '',
        'groups': ('git',),
        'tags': ('git', 'status'),
        'links': ('https://gist.github.com/tafkey/664266c00387c98631b3',),
        'source': '',
        'versions': (),
        'languages': (),
        'filename': '',
        'created': '2018-01-11T07:59:46.000001+00:00',
        'updated': '2018-01-11T07:59:46.000001+00:00',
        'uuid': '16cd5827-b6ef-4067-b5ac-3ceac07dde9f',
        'digest': '9e1949c2810df2a50137f0a4056b7992529b37632d9db0da7040d17bf16f5bd3'
    })

    REMOVE_CREATED = _DEFAULTS[_REMOVE]['created']
    REMOVE_UPDATED = _DEFAULTS[_REMOVE]['updated']
    FORCED_CREATED = _DEFAULTS[_FORCED]['created']
    FORCED_UPDATED = _DEFAULTS[_FORCED]['updated']
    EXITED_CREATED = _DEFAULTS[_EXITED]['created']
    EXITED_UPDATED = _DEFAULTS[_EXITED]['updated']
    NETCAT_CREATED = _DEFAULTS[_NETCAT]['created']
    NETCAT_UPDATED = _DEFAULTS[_NETCAT]['updated']
    UMOUNT_CREATED = _DEFAULTS[_UMOUNT]['created']
    UMOUNT_UPDATED = _DEFAULTS[_UMOUNT]['updated']
    INTERP_CREATED = _DEFAULTS[_INTERP]['created']
    INTERP_UPDATED = _DEFAULTS[_INTERP]['updated']

    if not DEFAULT_TIME == REMOVE_CREATED == REMOVE_UPDATED == FORCED_CREATED == FORCED_UPDATED:
        raise Exception('default content timestamps must be same - see \'Test case layouts and data structures\'')

    REMOVE_DIGEST = _DEFAULTS[_REMOVE]['digest']
    FORCED_DIGEST = _DEFAULTS[_FORCED]['digest']
    EXITED_DIGEST = _DEFAULTS[_EXITED]['digest']
    NETCAT_DIGEST = _DEFAULTS[_NETCAT]['digest']
    UMOUNT_DIGEST = _DEFAULTS[_UMOUNT]['digest']
    INTERP_DIGEST = _DEFAULTS[_INTERP]['digest']
    REMOVE_UUID = _DEFAULTS[_REMOVE]['uuid']
    FORCED_UUID = _DEFAULTS[_FORCED]['uuid']
    EXITED_UUID = _DEFAULTS[_EXITED]['uuid']
    NETCAT_UUID = _DEFAULTS[_NETCAT]['uuid']
    UMOUNT_UUID = _DEFAULTS[_UMOUNT]['uuid']
    INTERP_UUID = _DEFAULTS[_INTERP]['uuid']


    REMOVE = _DEFAULTS[_REMOVE]
    FORCED = _DEFAULTS[_FORCED]
    EXITED = _DEFAULTS[_EXITED]
    NETCAT = _DEFAULTS[_NETCAT]
    UMOUNT = _DEFAULTS[_UMOUNT]
    INTERP = _DEFAULTS[_INTERP]
    DEFAULT_SNIPPETS = (REMOVE, FORCED)

    TEMPLATE = Helper.read_template('snippet.txt').split('\n')
    TEMPLATE_DIGEST_EMPTY = 'b4bedc2603e3b9ea95bcf53cb7b8aa6efa31eabb788eed60fccf3d8029a6a6cc'
    TEMPLATE_TEXT = (
        '# Commented lines will be ignored.',
        '#',
        '# Add mandatory snippet below.',
        '',
        '',
        '# Add optional brief description below.',
        'Add brief title for content',
        '',
        '# Add optional description below.',
        'Add a description that defines the content in one chapter.',
        '',
        '# Add optional name below.',
        'example content handle',
        '',
        '# Add optional comma separated list of groups below.',
        'groups',
        '',
        '# Add optional comma separated list of tags below.',
        'example,tags',
        '',
        '# Add optional links below one link per line.',
        'https://www.example.com/add-links-here.html',
        '',
        '# Add optional source reference below.',
        'https://www.example.com/source.md',
        '',
        '# Add optional comma separated list of key-value versions below.',
        'example=3.9.0,python>=3',
        '',
        '# Add optional comma separated list of languages below.',
        'example-language',
        '',
        '# Add optional filename below.',
        'example-content.md',
        '',
        '# Meta',
        'category : snippet',
        'created  : 2018-02-02T02:02:02.000001+00:00',
        'digest   : b3e9bcf808ba86461bee74dca40f62e518ce306ecbac6a18adea78b30a0fe346',
        'updated  : 2018-02-02T02:02:02.000001+00:00',
        'uuid     : a1cd5827-b6ef-4067-b5ac-3ceac07dde9f',
        ''
    )
    TEMPLATE_MKDN = (
        '# Add brief title for content @groups',
        '',
        '> Add a description that defines the content in one chapter.',
        '',
        '> [1] https://www.example.com/add-links-here.html',
        '',
        '`$ Markdown commands are defined between backtics and prefixed by a dollar sign`',
        '',
        '## Meta',
        '',
        '> category  : snippet  ',
        'created   : 2018-02-02T02:02:02.000001+00:00  ',
        'digest    : 8d5193fea452d0334378a73ded829cfa27debea7ee87714d64b1b492d1a4601a  ',
        'filename  : example-content.md  ',
        'languages : example-language  ',
        'name      : example content handle  ',
        'source    : https://www.example.com/source.md  ',
        'tags      : example,tags  ',
        'updated   : 2018-02-02T02:02:02.000001+00:00  ',
        'uuid      : a1cd5827-b6ef-4067-b5ac-3ceac07dde9f  ',
        'versions  : example=3.9.0,python>=3  ',
        ''
    ) class Solution(object):  # pylint: disable=too-few-public-methods
    """Default solutions for testing."""

    _BEATS = 0
    _NGINX = 1
    _KAFKA = 2
    _KAFKA_MKDN = 3

    # Default time is same for the default content. See 'Test case layouts and
    # data structures' for more information.
    DEFAULT_TIME = '2017-10-20T11:11:19.000001+00:00'

    # Default content must be always set so that it reflects content stored
    # into database. For example the tags must be sorted in correct order.
    # This forces defining erroneous content in each test case. This improves
    # the readability and maintainability of failure testing.
    _DEFAULTS = ({
        'category': 'solution',
        'data':('################################################################################',
                '## Description',
                '################################################################################',
                '',
                '    # Debug Elastic Beats',
                '',
                '################################################################################',
                '## References',
                '################################################################################',
                '',
                '    # Enable logs from Filebeat',
                '    > https://www.elastic.co/guide/en/beats/filebeat/master/enable-filebeat-debugging.html',
                '',
                '################################################################################',
                '## Commands',
                '################################################################################',
                '',
                '    # Run Filebeat with full log level',
                '    $ ./filebeat -e -c config/filebeat.yml -d "*"',
                '',
                '################################################################################',
                '## Solutions',
                '################################################################################',
                '',
                '################################################################################',
                '## Configurations',
                '################################################################################',
                '',
                '################################################################################',
                '## Whiteboard',
                '################################################################################',
                ''),
        'brief': 'Debugging Elastic Beats',
        'description': 'Debug Elastic Beats',
        'name': '',
        'groups': ('beats',),
        'tags': ('Elastic', 'beats', 'debug', 'filebeat', 'howto'),
        'links': ('https://www.elastic.co/guide/en/beats/filebeat/master/enable-filebeat-debugging.html',),
        'source': '',
        'versions': (),
        'languages': (),
        'filename': 'howto-debug-elastic-beats.txt',
        'created': DEFAULT_TIME,
        'updated': DEFAULT_TIME,
        'uuid': '21cd5827-b6ef-4067-b5ac-3ceac07dde9f',
        'digest': '4346ba4c792474308bc66bd16d747875bef9b431044824987e302b726c1d298e'
    }, {
        'category': 'solution',
        'data':('################################################################################',
                '## Description',
                '################################################################################',
                '',
                '    # Instructions how to debug nginx.',
                '',
                '################################################################################',
                '## References',
                '################################################################################',
                '',
                '    # Official nginx debugging',
                '    > https://www.nginx.com/resources/admin-guide/debug/',
                '',
                '################################################################################',
                '## Commands',
                '################################################################################',
                '',
                '    # Test if nginx is configured with --with-debug',
                "    $ nginx -V 2>&1 | grep -- '--with-debug'",
                '',
                '    # Check the logs are forwarded to stdout/stderr and remove links',
                '    $ ls -al /var/log/nginx/',
                '    $ unlink /var/log/nginx/access.log',
                '    $ unlink /var/log/nginx/error.log',
                '',
                '    # Reloading nginx configuration',
                '    $ nginx -s reload',
                '',
                '################################################################################',
                '## Solutions',
                '################################################################################',
                '',
                '################################################################################',
                '## Configurations',
                '################################################################################',
                '',
                '    # Configuring nginx default.conf',
                '    $ vi conf.d/default.conf',
                '      upstream kibana_servers {',
                '          server kibana:5601;',
                '      }',
                '      upstream elasticsearch_servers {',
                '          server elasticsearch:9200;',
                '      }',
                '',
                '################################################################################',
                '## Whiteboard',
                '################################################################################',
                '',
                '    # Change nginx configuration',
                "    $ docker exec -i -t $(docker ps | egrep -m 1 'petelk/nginx' | awk '{print $1}') /bin/bash",
                ''),
        'brief': 'Debugging nginx',
        'description': 'Instructions how to debug nginx.',
        'name': '',
        'groups': ('nginx',),
        'tags': ('debug', 'howto', 'logging', 'nginx'),
        'links': ('https://www.nginx.com/resources/admin-guide/debug/', ),
        'source': '',
        'versions': (),
        'languages': (),
        'filename': 'howto-debug-nginx.txt',
        'created': DEFAULT_TIME,
        'updated': DEFAULT_TIME,
        'uuid': '22cd5827-b6ef-4067-b5ac-3ceac07dde9f',
        'digest': '6cfe47a8880a8f81b66ff6bd71e795069ed1dfdd259c9fd181133f683c7697eb'
    }, {
        'category': 'solution',
        'data':('################################################################################',
                '## Description',
                '################################################################################',
                '',
                '    # Investigating docker log driver and especially the Kafka plugin.',
                '',
                '################################################################################',
                '## References',
                '################################################################################',
                '',
                '    # Kube Kafka log driver',
                '    > https://github.com/MickayG/moby-kafka-logdriver',
                '',
                '    # Logs2Kafka',
                '    > https://groups.google.com/forum/#!topic/kubernetes-users/iLDsG85exRQ',
                '    > https://github.com/garo/logs2kafka',
                '',
                '################################################################################',
                '## Commands',
                '################################################################################',
                '',
                '    # Get logs from pods',
                '    $ kubectl get pods',
                '    $ kubectl logs kafka-0',
                '',
                '    # Install docker log driver for Kafka',
                '    $ docker ps --format "{{.Names}}" | grep -E \'kafka|logstash\'',
                '    $ docker inspect k8s_POD_kafka-0...',
                "    $ docker inspect --format '{{ .NetworkSettings.IPAddress }}' k8s_POD_kafka-0...",
                '    $ docker plugin install --disable mickyg/kafka-logdriver:latest',
                '    $ docker plugin set mickyg/kafka-logdriver:latest KAFKA_BROKER_ADDR="10.2.28.10:9092"',
                '    $ docker plugin inspect mickyg/kafka-logdriver',
                '    $ docker plugin enable mickyg/kafka-logdriver:latest',
                '    $ docker run --log-driver mickyg/kafka-logdriver:latest hello-world',
                '    $ docker plugin disable mickyg/kafka-logdriver:latest',
                '',
                '    # Get current docker log driver',
                "    $ docker info |grep 'Logging Driver' # Default driver",
                '    $ docker ps --format "{{.Names}}" | grep -E \'kafka|logstash\'',
                '    $ docker inspect k8s_POD_kafka-0...',
                "    $ docker inspect --format '{{ .NetworkSettings.IPAddress }}' k8s_POD_logstash...",
                "    $ docker inspect --format '{{ .NetworkSettings.IPAddress }}' k8s_POD_kafka-0...",
                '    $ docker inspect $(docker ps | grep POD | awk \'{print $1}\') | grep -E "Hostname|NetworkID',
                '    $ docker inspect $(docker ps | grep POD | awk \'{print $1}\') | while read line ; do egrep -E ' +
                '\'"Hostname"|"IPAddress"\' ; done | while read line ; do echo $line ; done',
                '',
                '################################################################################',
                '## Solutions',
                '################################################################################',
                '',
                '################################################################################',
                '## Configurations',
                '################################################################################',
                '',
                '    # Logstash configuration',
                '    $ vi elk-stack/logstash/build/pipeline/kafka.conf',
                '      input {',
                '          gelf {}',
                '      }',
                '',
                '      output {',
                '          elasticsearch {',
                '            hosts => ["elasticsearch"]',
                '          }',
                '          stdout {}',
                '      }',
                '',
                '    # Kafka configuration',
                '    $ vi elk-stack/logstash/build/pipeline/kafka.conf',
                '    kafka {',
                '        type => "argus.docker"',
                '        topics => ["dockerlogs"]',
                '        codec => "plain"',
                '        bootstrap_servers => "kafka:9092"',
                '        consumer_threads => 1',
                '    }',
                '',
                '################################################################################',
                '## Whiteboard',
                '################################################################################',
                ''),
        'brief': 'Testing docker log drivers',
        'description': 'Investigating docker log driver and especially the Kafka plugin.',
        'name': '',
        'groups': ('docker',),
        'tags': ('docker', 'driver', 'kafka', 'kubernetes', 'logging', 'logs2kafka', 'moby', 'plugin'),
        'links': ('https://github.com/MickayG/moby-kafka-logdriver',
                  'https://github.com/garo/logs2kafka',
                  'https://groups.google.com/forum/#!topic/kubernetes-users/iLDsG85exRQ'),
        'source': '',
        'versions': (),
        'languages': (),
        'filename': 'kubernetes-docker-log-driver-kafka.txt',
        'created': '2017-10-20T06:16:27.000001+00:00',
        'updated': '2017-10-20T06:16:27.000001+00:00',
        'uuid': '23cd5827-b6ef-4067-b5ac-3ceac07dde9f',
        'digest': 'ee3f2ab7c63d6965ac2531003807f00caee178f6e1cbb870105c7df86e6d5be2'
    }, {
        'category': 'solution',
        'data':('## Description',
                '',
                'Investigate docker log drivers and the logs2kafka log plugin.',
                '',
                '## References',
                '',
                '   ```',
                '   # Kube Kafka log driver',
                '   > https://github.com/MickayG/moby-kafka-logdriver',
                '   ```',
                '',
                '   ```',
                '   # Logs2Kafka',
                '   > https://groups.google.com/forum/#!topic/kubernetes-users/iLDsG85exRQ',
                '   > https://github.com/garo/logs2kafka',
                '   ```',
                '',
                '## Commands',
                '',
                '   ```',
                '   # Get logs from pods',
                '   $ kubectl get pods',
                '   $ kubectl logs kafka-0',
                '   ```',
                '',
                '   ```',
                '   # Install docker log driver for Kafka',
                '   $ docker ps --format "{{.Names}}" | grep -E \'kafka|logstash\'',
                '   $ docker inspect k8s_POD_kafka-0...',
                '   $ docker inspect --format \'{{ .NetworkSettings.IPAddress }}\' k8s_POD_kafka-0...',
                '   $ docker plugin install --disable mickyg/kafka-logdriver:latest',
                '   $ docker plugin set mickyg/kafka-logdriver:latest KAFKA_BROKER_ADDR="10.2.28.10:9092"',
                '   $ docker plugin inspect mickyg/kafka-logdriver',
                '   $ docker plugin enable mickyg/kafka-logdriver:latest',
                '   $ docker run --log-driver mickyg/kafka-logdriver:latest hello-world',
                '   $ docker plugin disable mickyg/kafka-logdriver:latest',
                '   ```',
                '',
                '   ```',
                '   # Get current docker log driver',
                '   $ docker info |grep \'Logging Driver\' # Default driver',
                '   $ docker ps --format "{{.Names}}" | grep -E \'kafka|logstash\'',
                '   $ docker inspect k8s_POD_kafka-0...',
                '   $ docker inspect --format \'{{ .NetworkSettings.IPAddress }}\' k8s_POD_logstash...',
                '   $ docker inspect --format \'{{ .NetworkSettings.IPAddress }}\' k8s_POD_kafka-0...',
                '   $ docker inspect $(docker ps | grep POD | awk \'{print $1}\') | grep -E "Hostname|NetworkID',
                '   $ docker inspect $(docker ps | grep POD | awk \'{print $1}\') | while read line ; do egrep -E \'"Hostname"|"IPAddress"\' ; done | while read line ; do echo $line ; done',  # noqa pylint: disable=line-too-long
                '   ```',
                '',
                '## Configurations',
                '',
                '   ```',
                '   # Logstash configuration',
                '   $ vi elk-stack/logstash/build/pipeline/kafka.conf',
                '     input {',
                '         gelf {}',
                '     }',
                '',
                '     output {',
                '         elasticsearch {',
                '           hosts => ["elasticsearch"]',
                '         }',
                '         stdout {}',
                '     }',
                '   ```',
                '',
                '   ```',
                '   # Kafka configuration',
                '   $ vi elk-stack/logstash/build/pipeline/kafka.conf',
                '   kafka {',
                '       type => "argus.docker"',
                '       topics => ["dockerlogs"]',
                '       codec => "plain"',
                '       bootstrap_servers => "kafka:9092"',
                '       consumer_threads => 1',
                '   }',
                '   ```',
                '',
                '## Solutions',
                '',
                '## Whiteboard',
                ''),
        'brief': 'Testing docker log drivers',
        'description': 'Investigate docker log drivers and the logs2kafka log plugin.',
        'name': '',
        'groups': ('docker',),
        'tags': ('docker', 'driver', 'kafka', 'kubernetes', 'logging', 'logs2kafka', 'moby', 'plugin'),
        'links': ('https://github.com/MickayG/moby-kafka-logdriver',
                  'https://github.com/garo/logs2kafka',
                  'https://groups.google.com/forum/#!topic/kubernetes-users/iLDsG85exRQ'),
        'source': '',
        'versions': (),
        'languages': (),
        'filename': 'kubernetes-docker-log-driver-kafka.mkdn',
        'created': '2019-01-04T10:54:49.265512+00:00',
        'updated': '2019-01-05T10:54:49.265512+00:00',
        'uuid': '24cd5827-b6ef-4067-b5ac-3ceac07dde9f',
        'digest': 'c54c8a896b94ea35edf6c798879957419d26268bd835328d74b19a6e9ce2324d'
    })

    BEATS_CREATED = _DEFAULTS[_BEATS]['created']
    BEATS_UPDATED = _DEFAULTS[_BEATS]['updated']
    NGINX_CREATED = _DEFAULTS[_NGINX]['created']
    NGINX_UPDATED = _DEFAULTS[_NGINX]['updated']
    KAFKA_CREATED = _DEFAULTS[_KAFKA]['created']
    KAFKA_UPDATED = _DEFAULTS[_KAFKA]['updated']
    KAFKA_MKDN_CREATED = _DEFAULTS[_KAFKA_MKDN]['created']
    KAFKA_MKDN_UPDATED = _DEFAULTS[_KAFKA_MKDN]['updated']

    if not DEFAULT_TIME == BEATS_CREATED == BEATS_UPDATED == NGINX_CREATED == NGINX_UPDATED:
        raise Exception('default content timestamps must be same - see \'Test case layouts and data structures\'')

    BEATS_DIGEST = _DEFAULTS[_BEATS]['digest']
    NGINX_DIGEST = _DEFAULTS[_NGINX]['digest']
    KAFKA_DIGEST = _DEFAULTS[_KAFKA]['digest']
    KAFKA_MKDN_DIGEST = _DEFAULTS[_KAFKA_MKDN]['digest']
    BEATS_UUID = _DEFAULTS[_BEATS]['uuid']
    NGINX_UUID = _DEFAULTS[_NGINX]['uuid']
    KAFKA_UUID = _DEFAULTS[_KAFKA]['uuid']
    KAFKA_MKDN_UUID = _DEFAULTS[_KAFKA_MKDN]['uuid']

    BEATS = _DEFAULTS[_BEATS]
    NGINX = _DEFAULTS[_NGINX]
    KAFKA = _DEFAULTS[_KAFKA]
    KAFKA_MKDN = _DEFAULTS[_KAFKA_MKDN]
    DEFAULT_SOLUTIONS = (BEATS, NGINX)

    TEMPLATE = Helper.read_template('solution.txt').split('\n')
    TEMPLATE_DIGEST_TEXT = 'be2ec3ade0e984463c1d3346910a05625897abd8d3feae4b2e54bfd6aecbde2d'
    TEMPLATE_DIGEST_MKDN = '073ea152d867cf06b2ee993fb1aded4c8ccbc618972db5c18158b5b68a5da6e4'

    TEMPLATE_TEXT = (
        '################################################################################',
        '## BRIEF  : Add brief title for content',
        '##',
        '## GROUPS : groups',
        '## TAGS   : example,tags',
        '## FILE   : example-content.md',
        '################################################################################',
        '',
        '',
        '################################################################################',
        '## Description',
        '################################################################################',
        '',
        '################################################################################',
        '## References',
        '################################################################################',
        '',
        '################################################################################',
        '## Commands',
        '################################################################################',
        '',
        '################################################################################',
        '## Configurations',
        '################################################################################',
        '',
        '################################################################################',
        '## Solutions',
        '################################################################################',
        '',
        '################################################################################',
        '## Whiteboard',
        '################################################################################',
        '',
        '################################################################################',
        '## Meta',
        '################################################################################',
        '',
        'category  : solution',
        'created   : 2017-10-14T19:56:31.000001+00:00',
        'digest    : 50c37862816a197c63b2ae72c511586c3463814509c0d5c7ebde534ce0209935',
        'languages : example-language',
        'name      : example content handle',
        'source    : https://www.example.com/source.md',
        'updated   : 2017-10-14T19:56:31.000001+00:00',
        'uuid      : a1cd5827-b6ef-4067-b5ac-3ceac07dde9f',
        'versions  : example=3.9.0,python>=3',
        ''
    )
    TEMPLATE_MKDN = (
        '# Add brief title for content @groups',
        '',
        '> Add a description that defines the content in one chapter.',
        '',
        '> ',
        '',
        '## Description',
        '',
        '## References',
        '',
        '## Commands',
        '',
        '## Configurations',
        '',
        '## Solutions',
        '',
        '## Whiteboard',
        '',
        '## Meta',
        '',
        '> category  : solution  ',
        'created   : 2017-10-14T19:56:31.000001+00:00  ',
        'digest    : 5facdc16dc81851c2f65b112a0921eb2f2db206c7756714efb45ba0026471f11  ',
        'filename  : example-content.md  ',
        'languages : example-language  ',
        'name      : example content handle  ',
        'source    : https://www.example.com/source.md  ',
        'tags      : example,tags  ',
        'updated   : 2017-10-14T19:56:31.000001+00:00  ',
        'uuid      : a1cd5827-b6ef-4067-b5ac-3ceac07dde9f  ',
        'versions  : example=3.9.0,python>=3  ',
        ''
    )

    _OUTPUTS = [(
        '',
        '   # Elastic,beats,debug,filebeat,howto',
        '   > https://www.elastic.co/guide/en/beats/filebeat/master/enable-filebeat-debugging.html',
        '',
        '   : ################################################################################',
        '   : ## Description',
        '   : ################################################################################',
        '   : ',
        '   :     # Debug Elastic Beats',
        '   : ',
        '   : ################################################################################',
        '   : ## References',
        '   : ################################################################################',
        '   : ',
        '   :     # Enable logs from Filebeat',
        '   :     > https://www.elastic.co/guide/en/beats/filebeat/master/enable-filebeat-debugging.html',
        '   : ',
        '   : ################################################################################',
        '   : ## Commands',
        '   : ################################################################################',
        '   : ',
        '   :     # Run Filebeat with full log level',
        '   :     $ ./filebeat -e -c config/filebeat.yml -d "*"',
        '   : ',
        '   : ################################################################################',
        '   : ## Solutions',
        '   : ################################################################################',
        '   : ',
        '   : ################################################################################',
        '   : ## Configurations',
        '   : ################################################################################',
        '   : ',
        '   : ################################################################################',
        '   : ## Whiteboard',
        '   : ################################################################################'
    )]
    BEATS_OUTPUT = _OUTPUTS[_BEATS]
import copy
import datetime
import traceback
import json
import mock
import pprintpp
from jsonschema.exceptions import ValidationError
from jsonschema.exceptions import SchemaError
from snippy.config.config import Config
from snippy.constants import Constants as Const
from snippy.content.collection import Collection
from snippy.meta import __docs__
from snippy.meta import __homepage__
from snippy.meta import __openapi__
from snippy.meta import __version__
from tests.lib.database import Database
from tests.lib.helper import Helper
from tests.lib.helper import Classproperty as classproperty
from tests.lib.reference import Reference
from tests.lib.snippet import Snippet
from tests.lib.solution import Solution
        """Get interp Snippet.

        Returns:
            dict: Resource attributes as stored in database.
        """

        return {attribute: Snippet.INTERP[attribute] for attribute in Snippet.INTERP}

    @classproperty
    def ebeats(cls):  # pylint: disable=no-self-argument, no-self-use
        """Get ebeats Solution.

        Returns:
            dict: Resource attributes as stored in database.
        """

        return {attribute: Solution.BEATS[attribute] for attribute in Solution.BEATS}

    @classproperty
    def dnginx(cls):  # pylint: disable=no-self-argument, no-self-use
        """Get dnginx Solution.

        Returns:
            dict: Resource attributes as stored in database.
        """

        return {attribute: Solution.NGINX[attribute] for attribute in Solution.NGINX}

    @classproperty
    def dkafka(cls):  # pylint: disable=no-self-argument, no-self-use
        """Get dkafka Solution.

        Returns:
            dict: Resource attributes as stored in database.
        """

        return {attribute: Solution.KAFKA[attribute] for attribute in Solution.KAFKA}

    @classproperty
    def kafkam(cls):  # pylint: disable=no-self-argument, no-self-use
        """Get kafkam Solution.

        Returns:
            dict: Resource attributes as stored in database.
        """

        return {attribute: Solution.KAFKA_MKDN[attribute] for attribute in Solution.KAFKA_MKDN}

    @classproperty
    def gitlog(cls):  # pylint: disable=no-self-argument, no-self-use
        """Get gitlog Reference.

        Returns:
            dict: Resource attributes as stored in database.
        """

        return {attribute: Reference.GITLOG[attribute] for attribute in Reference.GITLOG}

    @classproperty
    def regexp(cls):  # pylint: disable=no-self-argument, no-self-use
        """Get regexp Reference.

        Returns:
            dict: Resource attributes as stored in database.
        """

        return {attribute: Reference.REGEXP[attribute] for attribute in Reference.REGEXP}

    @classproperty
    def pytest(cls):  # pylint: disable=no-self-argument, no-self-use
        """Get pytest Reference.

        Returns:
            dict: Resource attributes as stored in database.
        """

        return {attribute: Reference.PYTEST[attribute] for attribute in Reference.PYTEST}


class Request(object):
    """Content represented as in a HTTP request."""

    @classproperty
    def remove(cls):  # pylint: disable=no-self-argument, no-self-use
        """Get remove Snippet.

        Returns:
            dict: Resource attributes as sent in HTTP request.
        """

        return {attribute: Snippet.REMOVE[attribute] for attribute in Helper.REQUEST_ATTRIBUTES}

    @classproperty
    def forced(cls):  # pylint: disable=no-self-argument, no-self-use
        """Get forced Snippet.

        Returns:
            dict: Resource attributes as sent in HTTP request.
        """

        return {attribute: Snippet.FORCED[attribute] for attribute in Helper.REQUEST_ATTRIBUTES}

    @classproperty
    def exited(cls):  # pylint: disable=no-self-argument, no-self-use
        """Get exited Snippet.

        Returns:
            dict: Resource attributes as sent in HTTP request.
        """

        return {attribute: Snippet.EXITED[attribute] for attribute in Helper.REQUEST_ATTRIBUTES}

    @classproperty
    def netcat(cls):  # pylint: disable=no-self-argument, no-self-use
        """Get netcat Snippet.

        Returns:
            dict: Resource attributes as sent in HTTP request.
        """


        return {attribute: Snippet.NETCAT[attribute] for attribute in Helper.REQUEST_ATTRIBUTES}