Source code for invenio_base.app

# -*- coding: utf-8 -*-
#
# This file is part of Invenio.
# Copyright (C) 2015, 2016 CERN.
#
# Invenio is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# Invenio 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
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Invenio; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
#
# In applying this license, CERN does not waive the privileges and immunities
# granted to it by virtue of its status as an Intergovernmental Organization or
# submit itself to any jurisdiction.

"""Invenio application factory."""

from __future__ import absolute_import, print_function

import logging
import os.path
import sys
import warnings

import click
import pkg_resources
from flask import Flask
from flask.cli import FlaskGroup
from flask.helpers import get_debug_flag

from .cmd import instance


def create_app_factory(app_name, config_loader=None,
                       extension_entry_points=None, extensions=None,
                       blueprint_entry_points=None, blueprints=None,
                       converter_entry_points=None, converters=None,
                       wsgi_factory=None, **app_kwargs):
    """Create a Flask application factory.

    The application factory will load Flask extensions and blueprints specified
    using both entry points and directly in the arguments. Loading order of
    entry points are not guaranteed and can happen in any order.

    :param app_name: Flask application name.
    :param config_loader: Callable which will be invoked on application
        creation in order to load the Flask configuration. See example below.
    :param extension_entry_points: List of entry points, which specifies Flask
        extensions that will be initialized only by passing in the Flask
        application object
    :param extensions: List of Flask extensions that can be initialized only by
        passing in the Flask application object.
    :param blueprint_entry_points: List of entry points, which specifies
        Blueprints that will be registered on the Flask application.
    :param blueprints: List of Blueprints that will be registered on the
        Flask application.
    :param converter_entry_points: List of entry points, which specifies
        Werkzeug URL map converters that will be added to
        ``app.url_map.converters``.
    :param converters: Map of Werkzeug URL map converter classes that will
        be added to ``app.url_map.converters``.
    :param wsgi_factory: A callable that will be passed the Flask application
        object in order to overwrite the default WSGI application (e.g. to
        install ``DispatcherMiddleware``).
    :param app_kwargs: Keyword arguments passed to :py:meth:`base_app`.
    :returns: Flask application factory.

    Example of a configuration loader:

    .. code-block:: python

       def my_config_loader(app, **kwargs):
           app.config.from_module('mysite.config')
           app.config.update(**kwargs)

    .. note::

       `Invenio-Config <https://pythonhosted.org/invenio-config>`_ provides a
       factory creating default configuration loader (see
       :func:`invenio_config.utils.create_config_loader`) which is sufficient
       for most cases.

    Example of a WSGI factory:

    .. code-block:: python

       def my_wsgi_factory(app):
           return DispatcherMiddleware(app.wsgi_app, {'/api': api_app})

    .. versionadded: 1.0.0
    """
    def _create_app(**kwargs):
        app = base_app(app_name, **app_kwargs)

        debug = kwargs.get('debug')
        if debug is not None:
            app.debug = debug

        # Load configuration
        if config_loader:
            config_loader(app, **kwargs)

        # Load URL converters.
        converter_loader(
            app,
            entry_points=converter_entry_points,
            modules=converters,
        )

        # Load application based on entrypoints.
        app_loader(
            app,
            entry_points=extension_entry_points,
            modules=extensions,
        )

        # Load blueprints
        blueprint_loader(
            app,
            entry_points=blueprint_entry_points,
            modules=blueprints,
        )

        # Replace WSGI application using factory if provided (e.g. to install
        # WSGI middleware).
        if wsgi_factory:
            app.wsgi_app = wsgi_factory(app, **kwargs)

        return app

    return _create_app


def create_cli(create_app=None):
    """Create CLI for ``inveniomanage`` command.

    :param create_app: Flask application factory.
    :returns: Click command group.

    .. versionadded: 1.0.0
    """
    def create_cli_app(info):
        """Application factory for CLI app.

        Internal function for creating the CLI. When invoked via
        ``inveniomanage`` FLASK_APP must be set.
        """
        if create_app is None:
            # Fallback to normal Flask behavior
            info.create_app = None
            app = info.load_app()
        else:
            app = create_app(debug=get_debug_flag())
        return app

    @click.group(cls=FlaskGroup, create_app=create_cli_app)
    def cli(**params):
        """Command Line Interface for Invenio."""
        pass

    # Add command for starting new Invenio instances.
    cli.add_command(instance)

    return cli


def app_loader(app, entry_points=None, modules=None):
    """Default application loader.

    :param entry_points: List of entry points providing to Flask extensions.
    :param modules: List of Flask extensions.

    .. versionadded: 1.0.0
    """
    _loader(app, lambda ext: ext(app), entry_points=entry_points,
            modules=modules)


def blueprint_loader(app, entry_points=None, modules=None):
    """Default blueprint loader.

    :param entry_points: List of entry points providing to Blueprints.
    :param modules: List of Blueprints.

    .. versionadded: 1.0.0
    """
    url_prefixes = app.config.get('BLUEPRINTS_URL_PREFIXES', {})
    _loader(app, lambda bp: app.register_blueprint(
        bp, url_prefix=url_prefixes.get(bp.name)
    ), entry_points=entry_points, modules=modules)


def converter_loader(app, entry_points=None, modules=None):
    """Default converter loader.

    :param entry_points: List of entry points providing to Blue.
    :param modules: Map of coverters.

    .. versionadded: 1.0.0
    """
    if entry_points:
        for entry_point in entry_points:
            for ep in pkg_resources.iter_entry_points(entry_point):
                try:
                    app.url_map.converters[ep.name] = ep.load()
                except Exception:
                    app.logger.error(
                        'Failed to initialize entry point: {0}'.format(ep))
                    raise

    if modules:
        app.url_map.converters.update(**modules)


def _loader(app, init_func, entry_points=None, modules=None):
    """Generic loader.

    Used to load and initialize entry points and modules using an custom
    initialization function.

    .. versionadded: 1.0.0
    """
    if entry_points:
        for entry_point in entry_points:
            for ep in pkg_resources.iter_entry_points(entry_point):
                try:
                    init_func(ep.load())
                except Exception:
                    app.logger.error(
                        'Failed to initialize entry point: {0}'.format(ep))
                    raise
    if modules:
        for m in modules:
            try:
                init_func(m)
            except Exception:
                app.logger.error('Failed to initialize module: {0}'.format(m))
                raise


def base_app(import_name, instance_path=None, static_folder=None,
             static_url_path='/static', template_folder='templates',
             instance_relative_config=True):
    """Invenio base application factory.

    If the instance folder does not exists, it will be created.

    :param import_name: The name of the application package.
    :param env_prefix: Environment variable prefix.
    :param instance_path: Instance path for Flask application.
    :param static_folder: Static folder path.
    :returns: Flask application instance.

    .. versionadded: 1.0.0
    """
    configure_warnings()

    # Create the Flask application instance
    app = Flask(
        import_name,
        instance_path=instance_path,
        instance_relative_config=instance_relative_config,
        static_folder=static_folder,
        static_url_path=static_url_path,
        template_folder=template_folder,
    )

    # Create instance path if it doesn't exists
    try:
        if instance_path and not os.path.exists(instance_path):
            os.makedirs(instance_path)
    except Exception:  # pragma: no cover
        app.logger.exception(
            'Failed to create instance folder: "{0}"'.format(instance_path)
        )

    return app


def configure_warnings():
    """Configure warnings by routing warnings to the logging system.

    It also unhides ``DeprecationWarning``.

    .. versionadded: 1.0.0
    """
    if not sys.warnoptions:
        # Route warnings through python logging
        logging.captureWarnings(True)

        # DeprecationWarning is by default hidden, hence we force the
        # "default" behavior on deprecation warnings which is not to hide
        # errors.
        warnings.simplefilter('default', DeprecationWarning)