class Cause(object):
    """Cause code services."""

    ALL_OK = 'OK'

    # HTTP status codes.
    HTTP_200 = '200 OK'
    HTTP_201 = '201 Created'
    HTTP_204 = '204 No Content'
    HTTP_400 = '400 Bad Request'
    HTTP_403 = '403 Forbidden'
    HTTP_405 = '405 Method Not Allowed'
    HTTP_404 = '404 Not Found'
    HTTP_409 = '409 Conflict'
    HTTP_500 = '500 Internal Server Error'

    HTTP_OK = HTTP_200
    HTTP_CREATED = HTTP_201
    HTTP_NO_CONTENT = HTTP_204
    HTTP_BAD_REQUEST = HTTP_400
    HTTP_FORBIDDEN = HTTP_403
    HTTP_NOT_FOUND = HTTP_404
    HTTP_METHOD_NOT_ALLOWED = HTTP_405
    HTTP_CONFLICT = HTTP_409
    HTTP_INTERNAL_SERVER_ERROR = HTTP_500
    OK_STATUS_LIST = (HTTP_OK, HTTP_CREATED, HTTP_NO_CONTENT)

    HTTP_200_OK = 200
    HTTP_201_CREATED = 201
    HTTP_204_NO_CONTENT = 204
    HTTP_404_NOT_FOUND = 404

    _list = {'errors': []}
    _logger = Logger.get_logger(__name__)

    @classmethod
    def reset(cls):
        """Reset cause to initial value."""

        cause = cls.get_message()
        cls._list = {'errors': []}

        return cause

    @classmethod
    def push(cls, status, message):
        """Append cause to list.

        Message will always contain only the string till the first newline.
        The reason is that the message may be coming from an exception which
        message may contain multiple lines. In this case it is always assumed
        that the first line contains the actual exception message. The whole
        message is always printed into log.

        Args:
            status (str): One of the predefined HTTP status codes.
            message (str): Description of the cause.

        Examples
        --------
        >>> Cause.push(Cause.HTTP_CREATED, 'content created')
        """

        # Optimization: Prevent setting the caller module and line number in case
        # of success causes. Reading of the line number requires file access that
        # is expensive and avoided in successful cases.
        caller = 'snippy.cause.cause.optimize:1'
        if status not in Cause.OK_STATUS_LIST:
            caller = cls._caller()
        cls._logger.debug('cause %s with message %s from %s', status, message, caller)
        cls._list['errors'].append({
            'status': int(status.split()[0]),
            'status_string': status,
            'module': caller,
            'title': message.splitlines()[0]
        })

    @classmethod
    def insert(cls, status, message):
        """Insert cause as a first cause.

        Args:
            status (str): One of the predefined HTTP status codes.
            message (str): Description of the cause.

        Examples
        --------
        >>> Cause.insert(Cause.HTTP_CREATED, 'content created')
        """

        cls.push(status, message)
        cls._list['errors'].insert(0, cls._list['errors'].pop())

    @classmethod
    def is_ok(cls):
        """Test if errors were detected.

        The status is considered ok in following cases:

          1. There are no errors at all.
          2. There are only accepted error codes.
          3. Content has been created and there are only 409 Conflict errors.

        The last case is a special case. It is considered as a successful
        case when there are multiple resources imported and some of them
        are already created.

        Returns:
            bool: Define if the cause list can be considered ok.
        """

        is_ok = False
        if not cls._list['errors']:
            is_ok = True
        elif all(error['status_string'] in Cause.OK_STATUS_LIST for error in cls._list['errors']):
            is_ok = True
        elif (any(error['status_string'] == cls.HTTP_CREATED for error in cls._list['errors']) and
              all(error['status_string'] in (cls.HTTP_CREATED, cls.HTTP_CONFLICT) for error in cls._list['errors'])):
            is_ok = True

        return is_ok

    @classmethod
    def http_status(cls):
        """Return the HTTP status."""

        status = Cause.HTTP_OK
        if cls._list['errors']:
            status = cls._list['errors'][0]['status_string']

        return status

    @classmethod
    def json_message(cls):
        """Return errors in JSON data structure."""

        response = cls._list
        response['meta'] = {
            'version': __version__,
            'homepage': __homepage__,
            'docs': __docs__,
            'openapi': __openapi__
        }

        return response

    @classmethod
    def get_message(cls):
        """Return cause message.

        Cause codes follow the same rules as the logs with the title or
        message. If there are variables within the message, the variables
        are separated with colon. The end user message is beautified so
        that if there is more than one colon, it indicates that variable
        is in the middle of the message. This is not considered good layout
        for command line interface messages.

        How ever, if there is only one colon, it is used to sepatate the
        last part which is considered clear for user.

        Because of these rules, the colon delimiters are removed only if
        there is more than one.

        Examples:

            1. cannot use empty content uuid for: delete :operation
            2. cannot find content with content uuid: 1234567
        """

        cause = Cause.ALL_OK
        if not cls.is_ok():
            message = cls._list['errors'][0]['title']
            if message.count(':') > 1:
                message = cls._list['errors'][0]['title'].replace(':', '')
            cause = 'NOK: ' + message

        return cause

    @classmethod
    def print_message(cls):
        """Print cause message."""

        Logger.print_status(cls.get_message())

    @classmethod
    def print_failure(cls):
        """Print only failure message."""

        if not cls.is_ok():
            Logger.print_status(cls.get_message())

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

        for idx, cause in enumerate(cls._list['errors']):
            print('cause[%d]:' % idx)
            print('  status : %s\n'
                  '  string : %s\n'
                  '  module : %s\n'
                  '  title  : %s\n' % (cause['status'], cause['status_string'], cause['module'], cause['title']))

    @classmethod
    def _is_internal_error(cls):
        """Test if internal error was detected."""

        if any(error['status_string'] == cls.HTTP_INTERNAL_SERVER_ERROR for error in cls._list['errors']):
            return True

        return False

    @staticmethod
    def _caller():
        """Get caller module and code line."""

        # Optimization: Inspect.stack reads source code file that generates
        # expensive file access. The contenxt loading can be switched off
        # with stack(0) setting /1/. A bit more efficient way is to use
        # sys._getframe that is according to /2/ four times faster the
        # stack(0). Testing shows that there is a noticeable difference
        # but not that much.
        #
        # Try to avoid calling this method for performance reasons.
        #
        # /1/ https://stackoverflow.com/a/17407257
        # /2/ https://stackoverflow.com/a/45196608
        frame = sys._getframe(2)  # pylint: disable=protected-access
        info = inspect.getframeinfo(frame)
        module = inspect.getmodule(frame)
        location = module.__name__ + ':' + str(info.lineno)

        return location 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 Content(ContentBase):
    """Content management."""

    def __init__(self, storage, category, run_cli=True):
        super(Content, self).__init__(storage, category, run_cli) class Fields(ContentBase):
    """Content fields."""

    def __init__(self, storage, category, run_cli=False):
        super(Fields, self).__init__(storage, category, run_cli) class CustomGunicornLogger(GunicornLogger):  # pylint: disable=too-few-public-methods
    """Custom logger for Gunicorn HTTP server."""

    def setup(self, cfg):
        """Custom setup.

        Disable all handlers under the 'gunicorn' namespace and prevent log
        propagation to root logger. The loggers under the 'snippy' namespace
        will take care of the log writing for Gunicorn server.

        Both Gunicor error and access log categories are printed from the same
        namespace. In case of 'snippy.server.gunicorn.error', informative logs
        in JSON format would have this in the class ``name`` attribute which
        is considered to be misleading for other than error logs.

        Args:
            cfg (obj): The Gunicorn server class Config() object.
        """

        super(CustomGunicornLogger, self).setup(cfg)
        self._remove_handlers(self.error_log)
        self._remove_handlers(self.access_log)
        logging.getLogger('gunicorn').propagate = False
        self.error_log = Logger.get_logger('snippy.server.gunicorn')
        self.access_log = Logger.get_logger('snippy.server.gunicorn')

    @staticmethod
    def _remove_handlers(logger):
        """Remove handlers.

        Args:
            logger (obj): Logger object returned from logging.getLogger.
        """

        handlers = logger.handlers
        for handler in handlers:
            handler.close()
            logger.removeHandler(handler)
        logger.setLevel(logging.NOTSET)
        logger.propagate = True class Logger(object):
    """Global logging service."""

    # Default maximum length of log message.
    DEFAULT_LOG_MSG_MAX = 80

    # Maximum length of log message for safety and security reasons.
    SECURITY_LOG_MSG_MAX = 10000

    # Custom security log level.
    SECURITY = logging.CRITICAL + 10

    # Unique operation ID that identifies logs for each operation.
    SERVER_OID = format(getrandbits(32), "08x")

    # Custom log format.
    LOG_FORMAT = '%(asctime)s %(appname)s[%(process)04d] [%(levelname).1s] [%(oid)s]: %(message)s'

    RE_MATCH_LEADING_WHITESPACES = re.compile(r'''
        (^\s+)    # Match leading whitespaces on every line of a multiline string.
        ''', re.MULTILINE | re.VERBOSE)

    # Global logger configuration.
    CONFIG = {
        'debug': False,
        'log_json': False,
        'log_msg_max': DEFAULT_LOG_MSG_MAX,
        'quiet': False,
        'very_verbose': False
    }

    # Use severity level names from RFC 5424 /1/. The level name is printed
    # with one letter when debug logs are in text mode. The JSON formatted
    # logs will contain full length log severity level name.
    #
    # The security level is a custom log level only for security events.
    #
    # /1/ https://en.wikipedia.org/wiki/Syslog#Severity_level
    logging.SECURITY = SECURITY
    logging.addLevelName(logging.SECURITY, 'security')
    logging.addLevelName(logging.CRITICAL, 'crit')
    logging.addLevelName(logging.ERROR, 'err')
    logging.addLevelName(logging.WARNING, 'warning')
    logging.addLevelName(logging.INFO, 'info')
    logging.addLevelName(logging.DEBUG, 'debug')

    # Ignore exceptions from logger to prevent them to be shown to end user.
    # For example 'broken pipe' errors with grep can cause these when logs
    # are enabled.
    #
    # There was an effort to implement a custom StreamHandler to write log
    # messages to stderr instead of stdout if there is a broken pipe error.
    # The problem was that the 'broken pipe' exception does not exist in
    # Python 2 and thus making a generic code is difficult.
    logging.raiseExceptions = False

    @classmethod
    def get_logger(cls, name=__name__):
        """Get logger.

        A custom logger adapater is returned to support a custom log level
        and additional logging parameters.

        Args:
            name (str): Name of the module that requests a Logger.

        Returns:
            obj: CustomLoggerAdapter logger to be used by caller.
        """

        logger = logging.getLogger(name)
        if not logger.handlers:
            formatter = CustomFormatter(Logger.LOG_FORMAT)
            handler = logging.StreamHandler(stream=sys.stdout)
            handler.setFormatter(formatter)
            handler.addFilter(CustomFilter())
            logger.addHandler(handler)
        logger = CustomLoggerAdapter(logger, {'appname': 'snippy', 'oid': Logger.SERVER_OID})

        return logger

    @classmethod
    def configure(cls, config):
        """Set and update logger configuration.

        The ``debug`` and ``very_verbose`` options have precedence over the
        ``quiet`` option. That is, either of the debug options are enabled,
        the quiet option does not have any effect.

        Args:
            config (dict): Logger configuration dictionary.

        Examples
        --------
        >>> Logger.configure({'debug': True,
        >>>                   'log_json': True,
        >>>                   'log_msg_max': Logger.DEFAULT_LOG_MSG_MAX,
        >>>                   'quiet': False,
        >>>                   'very_verbose': False})
        """

        cls.CONFIG['debug'] = config['debug']
        cls.CONFIG['log_json'] = config['log_json']
        cls.CONFIG['log_msg_max'] = config['log_msg_max']
        cls.CONFIG['quiet'] = config['quiet']
        cls.CONFIG['very_verbose'] = config['very_verbose']
        if cls.CONFIG['log_msg_max'] > Logger.SECURITY_LOG_MSG_MAX:
            cls.CONFIG['log_msg_max'] = Logger.DEFAULT_LOG_MSG_MAX

        # Set the effective log level for all the loggers created under the
        # 'snippy' namespace. This relies on that the module level logger
        # does not set the level and it remains as NOTSET. This causes
        # module level logger to propagate the log record to parent logger
        # where it eventually reaches the 'snippy' level that is just below
        # the 'root' level logger.
        #
        # The 'disabled' flag prevents also the critical level logs.
        #
        # Note! The below settings manage also the Gunicorn server logs.
        #       There is a custom logger set that causes the Gunicorn logs
        #       to be formatted by Snippy logging framework.
        logging.getLogger('snippy').disabled = True
        logging.getLogger('snippy').setLevel(logging.CRITICAL)
        if cls.CONFIG['debug'] or config['very_verbose']:
            logging.getLogger('snippy').disabled = False
            logging.getLogger('snippy').setLevel(logging.DEBUG)

        if config['log_msg_max'] > Logger.SECURITY_LOG_MSG_MAX:
            Logger.get_logger().debug('log message length: %s :cannot exceed security limit: %s',
                                      config['log_msg_max'], Logger.SECURITY_LOG_MSG_MAX)

        Logger._update()

    @staticmethod
    def reset():
        """Reset log level to default."""

        logging.getLogger('snippy').disabled = True
        logging.getLogger('snippy').setLevel(logging.WARNING)

    @staticmethod
    def remove():
        """Delete all logger handlers."""

        # Remove all handlers. This is needed for testing. This is related
        # to defining the stdout for StreamHandler and to the way how Pytest
        # uses stdout when capsys is used /1/. More information from Python
        # issue /2/.
        #
        # /1/ https://github.com/pytest-dev/pytest/issues/14
        # /2/ https://bugs.python.org/issue6333
        items = list(logging.root.manager.loggerDict.items())
        for name, logger in items:
            if 'snippy' in name:
                logger.handlers = []

    @classmethod
    def refresh_oid(cls):
        """Refresh operation ID (OID).

        The OID is used to separate logs within one operation. The helps
        post-processing of the logs by allowing for example querying all
        the logs in failing operation.
        """

        cls.SERVER_OID = format(getrandbits(32), "08x")

    @classmethod
    def print_stdout(cls, message):
        """Print output to stdout.

        Take care of nasty details like broken pipe when printing to
        stdout.

        Args:
            message (str): Text string to be printed to stdout.
        """

        # The signal handler manipulation and flush setting below prevents
        # 'broken pipe' errors with grep. For example incorrect parameter
        # usage in grep may cause this. See below listed references [1] and
        # [2] with examples that can be used in manual testing testing.
        #
        # NOTE! Catching broken pipe signal can be dangerous [3]. This is
        #       not changed because the alternative is problematic with
        #       Python 2 [3].
        #
        # [1] https://stackoverflow.com/a/16865106
        # [2] https://stackoverflow.com/a/26738736
        # [3] https://stackoverflow.com/a/35761190
        #
        # $ snippy search --sall '--all' --filter crap | grep --all
        # $ snippy search --sall 'test' --filter test -vv | grep --all
        # $ snippy --server-host 127.0.0.1:8080 -vv | grep get
        # $ snippy --help | head -n 20
        # $ snippy --help | tail -n 20
        # $ snippy search --sall . | head -n 20
        # $ snippy search --sall . | tail -n 20
        # $ snippy search --sall . -vv | head -n 20
        # $ snippy search --sall . -vv | tail -n 20
        if message:
            try:
                signal_sigpipe = getsignal(SIGPIPE)
                signal(SIGPIPE, SIG_DFL)
            except (NameError, ValueError):
                pass  # SIGPIPE is not available in Windows.
            print(message)
            sys.stdout.flush()
            try:
                signal(SIGPIPE, signal_sigpipe)
            except (NameError, UnboundLocalError, ValueError):
                pass  # SIGPIPE is not available in Windows.

    @classmethod
    def print_status(cls, status):
        """Print status information like exit cause or server running.

        Print user formatted log messages unless the JSON log formating is
        enabled. The ``debug`` and ``very_verbose`` options have precedence
        over the quiet option.

        If JSON logs are used, the format of the log must always be JSON.
        This is important for server installation where post processing of
        logs might be done elsewhere and incorrectly formatted logs may be
        discarded or cause errors.

        In order to post process a JSON log, the dictionary structure must
        always follow the same format. Because of this, the log is pushed
        always as a debug level log regardless of the log level to get the
        formatting done.

        Args:
            status (str): Status to be printed on stdout.
        """

        if Logger.CONFIG['log_json'] and not cls.CONFIG['quiet']:
            level = logging.getLogger('snippy').getEffectiveLevel()
            logging.getLogger('snippy').setLevel(logging.DEBUG)
            Logger.get_logger().debug('%s', status)
            logging.getLogger('snippy').setLevel(level)
        elif logging.getLogger('snippy').getEffectiveLevel() == logging.DEBUG:
            Logger.get_logger().debug(status)
        elif not cls.CONFIG['quiet']:
            cls.print_stdout(status)

    @staticmethod
    def timeit(method=None, refresh_oid=False):
        """Time method by measuring it latency.

        The operation ID (OID) is refreshed at the end.

        Args:
            method (str): Name of the method calling the timeit.
            refresh_oid (bool): Define if operation ID is refreshed or not.

        Returns:
            obj: Timeit wrapper function for decorators.
        """

        def _timeit(method):

            @wraps(method)
            def timed(*args, **kwargs):
                """Wrapper to measure latency."""

                start = time.time()
                if refresh_oid:
                    Logger.refresh_oid()
                Logger.get_logger().debug('run method: %s', method.__name__)
                result = method(*args, **kwargs)
                Logger.get_logger().debug('end method: %s :duration: %.6fs', method.__name__, (time.time() - start))

                return result

            return timed

        if method:
            return _timeit(method)

        return _timeit

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

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

        Returns:
            str: Same log message but without ANSI escape codes.
        """

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

    @staticmethod
    def debug():
        """Debug Logger by printing logging hierarchy."""

        from logging_tree import printout  # pylint: disable=bad-option-value, import-outside-toplevel
        printout()

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

        items = list(logging.root.manager.loggerDict.items())
        for _, logger in items:
            for handler in getattr(logger, 'handlers', ()):
                logger.removeHandler(handler)
                formatter = CustomFormatter(Logger.LOG_FORMAT)
                handler = logging.StreamHandler(stream=sys.stdout)
                handler.setFormatter(formatter)
                handler.addFilter(CustomFilter())
                logger.addHandler(handler) class GunicornServer(gunicorn.app.base.BaseApplication):  # pylint: disable=abstract-method
    """Gunicorn WSGI HTTP server."""

    def __init__(self, app, options):
        self.options = options
        self.application = app
        super(GunicornServer, self).__init__()

    def load_config(self):
        """Load configuration."""

        for key in self.options.keys():
            self.cfg.set(key, self.options[key])

    def load(self):
        """Load configuration."""

        return self.application

    @staticmethod
    def post_worker_init(worker):
        """Gunicorn worker initialized hook.

        This is called by the Gunicorn framework after a worker has been
        initialized. This is used to tell user that the server is running.
        Gunicorn server listen IP address is printed from workber because
        of a use where a random free ephemeral port is selected by OS.

        Args:
            worker (obj): Gunicorn Worker() class object.
        """

        listeners = ','.join([str(l) for l in worker.sockets])
        Logger.print_status('snippy server running at: {}'.format(listeners))

    @staticmethod
    def pre_fork(server, worker):
        """Gunicorn worker pre-fork hook.

        This hook is used to transfer the worker real port to the server to
        be used when the server close. In case ephemeral ports allocated by
        the operating system is used, the port is not known by the Snippy
        configuration.

        Because the server and worker are difference processes, it is not
        possible to transfer the data from worker after it has been forked.

        Args:
            server (obj): Gunicorn Arbiter() class object.
            worker (obj): Gunicorn Worker() class object.
        """

        listeners = ','.join([str(l) for l in worker.sockets])
        server.app.options['snippy_host'] = listeners

    @staticmethod
    def pre_request(_, __):
        """Gunicorn pre-request hook.

        This hook is used to prevent a log from the Gunicorn with incorrect
        operation ID (OID).  Without this, the first log message for each
        HTTP request would contain OID from the previous HTTP request.
        """

    @staticmethod
    def on_exit(server):
        """Gunicorn server exit hook.

        This is used to tell user that the server has been stopped.

        Args:
            server (obj): Gunicorn Arbiter() class object.
        """

        Logger.print_status('snippy server stopped at: {}'.format(server.app.options['snippy_host'])) class ApiAttributes(object):
    """Access unique resource attributes."""

    def __init__(self, content):
        self._logger = Logger.get_logger(__name__)
        self._category = content.category
        self._content = content

    @Logger.timeit(refresh_oid=True)
    def on_get(self, request, response):
        """Search unique resource attributes.

        Search is made from all content categories by default.

        Args:
            request (obj): Falcon Request().
            response (obj): Falcon Response().
        """

        self._logger.debug('run: %s %s', request.method, request.uri)
        if 'scat' not in request.params:
            request.params['scat'] = Const.CATEGORIES
        api = Api(self._category, Api.UNIQUE, request.params)
        Config.load(api)
        self._content.run()
        if not self._content.uniques:
            Cause.push(Cause.HTTP_NOT_FOUND, 'cannot find unique fields for %s attribute' % self._category)
        if Cause.is_ok():
            response.content_type = ApiResource.MEDIA_JSON_API
            response.body = Generate.fields(self._category, self._content.uniques, request, response)
            response.status = Cause.http_status()
        else:
            response.content_type = ApiResource.MEDIA_JSON_API
            response.body = Generate.error(Cause.json_message())
            response.status = Cause.http_status()

        Cause.reset()
        self._logger.debug('end: %s %s', request.method, request.uri)

    @staticmethod
    @Logger.timeit(refresh_oid=True)
    def on_post(request, response):
        """Create new field."""

        ApiNotImplemented.send(request, response)

    @staticmethod
    @Logger.timeit(refresh_oid=True)
    def on_put(request, response):
        """Change field."""

        ApiNotImplemented.send(request, response)

    @staticmethod
    @Logger.timeit(refresh_oid=True)
    def on_delete(request, response):
        """Delete field."""

        ApiNotImplemented.send(request, response)

    @staticmethod
    @Logger.timeit(refresh_oid=True)
    def on_options(_, response):
        """Respond with allowed methods."""

        response.status = Cause.HTTP_200
        response.set_header('Allow', 'GET,OPTIONS') class ApiHello(ApiResource):
    """Hello API."""

    def on_get(self, request, response):
        """Get server /hello API endpoint.

        The route is not measured and logs must not be printed from here if
        request came from the server healthcheck from the same host. This is
        the case when Docker container hosts the server and the healtcheck in
        the same container but in different processes.

        If the server in Docker container uses the ``--net=host`` option, it
        prevents the logs from HTTP request to this API endpoints because the
        server and host use the same network. This is accepted behaviour.

        As of now, it is considered that constant server healthcheck must not
        flood the logs with successful healthcheck. This would lose important
        logs for troubleshooting.

        For the normal /hello API call the logs must be printed in order to
        trace calls and problems with the /hello API. It is considered that
        external clients will use the /hello instead of the server base path.

        The logs are disabled based on local IP address to generate logs for
        remote healtchecks. External clients may try to flood the server so
        it is important to log the external events.
        """

        hello = {
            'meta': {
                'version': __version__,
                'homepage': __homepage__,
                'docs': __docs__,
                'openapi': __openapi__
            }
        }
        if request.netloc != Config.server_host:
            self._logger.debug('run: %s %s', request.method, request.uri)
        response.content_type = ApiResource.MEDIA_JSON_API
        response.body = Generate.dumps(hello)
        response.status = Cause.http_status()
        if request.netloc != Config.server_host:
            self._logger.debug('end: %s %s', request.method, request.uri)

    @Logger.timeit(refresh_oid=True)
    def on_options(self, request, response):
        """Respond with allowed methods for Hello!"""

        self._logger.debug('run: %s %s', request.method, request.uri)
        response.status = Cause.http_status()
        response.set_header('Allow', 'GET')
        self._logger.debug('end: %s %s', request.method, request.uri) class ApiContent(ApiResource):
    """Access API resources.""" class ApiContentId(ApiResourceId):
    """Access API resources with resource ID.""" class ApiContentIdField(ApiResourceIdField):
    """Access API content with resource ID and attribute."""
import sys
import ssl
import falcon
from signal import signal, getsignal, SIG_DFL
    from signal import SIGPIPE
    from urllib.parse import urljoin
    from urlparse import urljoin
from snippy.cause import Cause
from snippy.config.config import Config
from snippy.constants import Constants as Const
from snippy.content.content import Content
from snippy.content.fields import Fields
from snippy.logger import CustomGunicornLogger
from snippy.logger import Logger
from snippy.server.gunicorn_server import GunicornServer as SnippyServer
from snippy.server.rest.api_fields import ApiAttributes
from snippy.server.rest.api_hello import ApiHello
from snippy.server.rest.api_content import ApiContent
from snippy.server.rest.api_content import ApiContentId
from snippy.server.rest.api_content import ApiContentIdField
# -*- coding: utf-8 -*-
#
#  SPDX-License-Identifier: AGPL-3.0-or-later
#
#  snippy - software development and maintenance notes manager.
#  Copyright 2017-2020 Heikki J. Laaksonen  <laaksonen.heikki.j@gmail.com>
#
#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU Affero General Public License as published
#  by the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU Affero General Public License for more details.
#
#  You should have received a copy of the GNU Affero General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""server: JSON RESTish API server."""

try:
except ImportError:
    pass  # SIGPIPE is not available in Windows.

try:
except ImportError:



class Server(object):  # pylint: disable=too-few-public-methods
    """RESTish API Server."""

    def __init__(self, storage):
        self._logger = Logger.get_logger(__name__)
        self.api = None
        self.storage = storage

    def run(self):
        """Run Snippy API server."""

        options = {
            'bind': Config.server_host,
            'ca_certs': Config.server_ssl_ca_cert,
            'certfile': Config.server_ssl_cert,
            'ciphers': 'ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA',
            'keyfile': Config.server_ssl_key,
            'logger_class': CustomGunicornLogger,
            'on_exit': SnippyServer.on_exit,
            'post_worker_init': SnippyServer.post_worker_init,
            'pre_fork': SnippyServer.pre_fork,
            'pre_request': SnippyServer.pre_request,
            'ssl_version': ssl.PROTOCOL_TLSv1_2,
            'workers': 1
        }
        self._logger.debug('run rest api server application with base path: %s', Config.server_base_path_rest)
        try:
            self.api = falcon.API(media_type='application/vnd.api+json')
        except AttributeError:
            raise ImportError
        snippet = Content(self.storage, Const.SNIPPET, run_cli=False)
        solution = Content(self.storage, Const.SOLUTION, run_cli=False)
        reference = Content(self.storage, Const.REFERENCE, run_cli=False)
        groups = Fields(self.storage, Const.GROUPS, run_cli=False)
        tags = Fields(self.storage, Const.TAGS, run_cli=False)
        self.api.req_options.media_handlers.update({'application/vnd.api+json': falcon.media.JSONHandler()})  # noqa pylint: disable=no-member, line-too-long
        self.api.resp_options.media_handlers.update({'application/vnd.api+json': falcon.media.JSONHandler()})  # noqa pylint: disable=no-member, line-too-long
        self.api.add_route(Config.server_base_path_rest.rstrip('/'), ApiHello())
        self.api.add_route(urljoin(Config.server_base_path_rest, 'hello'), ApiHello())
        self.api.add_route(urljoin(Config.server_base_path_rest, 'snippets'), ApiContent(snippet))
        self.api.add_route(urljoin(Config.server_base_path_rest, 'snippets/{identity}'), ApiContentId(snippet))
        self.api.add_route(urljoin(Config.server_base_path_rest, 'snippets/{identity}/{field}'), ApiContentIdField(snippet))
        self.api.add_route(urljoin(Config.server_base_path_rest, 'solutions'), ApiContent(solution))
        self.api.add_route(urljoin(Config.server_base_path_rest, 'solutions/{identity}'), ApiContentId(solution))
        self.api.add_route(urljoin(Config.server_base_path_rest, 'solutions/{identity}/{field}'), ApiContentIdField(solution))

        self.api.add_route(urljoin(Config.server_base_path_rest, 'references'), ApiContent(reference))