Source code for ourobori.apps.http

"""
This module contains functions required to create
the ``app.py`` for your own http-service based on ourobori.
"""


from argparse import Namespace
import asyncio
from pathlib import Path
from typing import Callable
from typing import List
from typing import NoReturn
from typing import Optional
from typing import Tuple
from typing import Union

from aiohttp import web
import aiohttp_swagger
import attr
try:
    # import aiohttp_debugtoolbar if it is available so it can be used
    # in debugmode during development
    import aiohttp_debugtoolbar
    HAS_DEBUGTOOLBAR = True
except ModuleNotFoundError:
    HAS_DEBUGTOOLBAR = False

from ourobori.apps.dependencies.dbs import PSQLDataBase
from ourobori.apps.dependencies.services import RestService
from ourobori.apps.dependencies.hosts import Host
from ourobori.apps.middlewares import error_middleware
from ourobori.apps.options import build_arguments
from ourobori.services.logger import default_logger
from ourobori.services.rules import BaseRules


# replace asyncios event loop by uvloop to speed up loops
try:
    import uvloop
    asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
except ModuleNotFoundError:
    pass


async def _create_app(
        *,
        routes,
        rules: BaseRules,
        options: dict,
        swagger_definition: Optional[str] = None,
        dependencies: Optional[List[Union[Host,
                                          RestService,
                                          PSQLDataBase]]] = None,
        middlewares: Optional[list] = [error_middleware]) -> web.Application:
    """
    Creates the app for the service using the passed ``middlewares``.
    The passed ``rules`` and ``options`` are combined to be available in
    the service-apis at ``request.app.rules``.
    The logger logs either to the path defined in the argument ``--logfile``
    or the path defined at ``rules.logfile`` is set to the ``app.logger``
    to be available in the service-apis at ``request.app.logger``.
    Additionaly the logs will be redirected to logstash if a
    logstash-connection is defined in the rules.
    The routes for the service are extended by the static-routes ``/apidoc``
    and / or ``/client-apidoc`` if triggered by the flags ``--apidoc`` and
    ``--client_apidoc`` in the options.
    All other routes should be defined in the passed routes.
    Finally the resulting app is returned.

    Parameters:
        routes: The routes for the service to run.
        rules: The servicerules to use
        options: The options for the service.  As default the options are
            build using :func:`ourobori.apps.options.build_arguments`.
        swagger_definition: The path to the swagger_definition-file.
        dependencies: List of the dependencies of the service.
        middlewares: The middlewares to use.

    Returns:
        app: The resulting app
    """
    if not dependencies:
        dependencies = []

    # define the app used by the service
    app = web.Application(middlewares=middlewares)

    # applying arguments passed to the service-startup to the service.app
    # and the rules
    for param, value in options.items():
        setattr(app, param, value)
        try:
            rules.__getattribute__(param)
            if value:
                changes = {f'{param}': value}
                rules = attr.evolve(rules, **changes)
        except AttributeError:
            pass

    app.rules = rules

    # define logger
    custom_logfile = options.get('logfile')
    logger_args = dict(mode=options['mode'],
                       servicename=rules.servicename,
                       debug=options['debugmode'] or False)
    if rules.logstash_conn:
        logstash_connection = options['logstash'] or rules.logstash_conn
        logstash_host, logstash_port = logstash_connection.split(":")
        logger_args['logstash_connection'] = dict(host=logstash_host,
                                                  port=int(logstash_port))
    if custom_logfile:
        app.logger = default_logger(logfile_path=Path(custom_logfile),
                                    **logger_args)
    else:
        app.logger = default_logger(logfile_path=rules.logfile,
                                    **logger_args)

    # dependencies passed to the app-startup are checked to be alive and
    # then added to the dependencies inlcuded inside the app
    app['dependencies'] = {}
    for dependency in dependencies:
        if isinstance(dependency, PSQLDataBase):
            await dependency.prepare(app)
            await dependency.is_alive(app)
        elif isinstance(dependency, RestService):
            await dependency.is_alive()
        elif isinstance(dependency, Host):
            await dependency.is_alive()
        app['dependencies'][dependency.name] = dependency

    # add the routes to the documentation of the service created by sphinx
    if options['apidoc']:
        routes.static(prefix='/apidoc',
                      path=str(rules.apidoc_location.absolute()),
                      follow_symlinks=True)
    if options['client_apidoc']:
        routes.static(prefix='/client-apidoc',
                      path=str(rules.client_apidoc_location.absolute()),
                      follow_symlinks=True)

    # add service-routes
    app.add_routes(routes)

    # add swagger-support to be available at http://localhost:<PORT>/api/doc
    if swagger_definition:
        aiohttp_swagger.setup_swagger(
            app,
            swagger_from_file=swagger_definition)

    # if the service is run in debugmode the debugtoolbar and zipkin
    # are enabled if they are installed
    if options['debugmode']:
        if HAS_DEBUGTOOLBAR:
            aiohttp_debugtoolbar.setup(app)

    return app


[docs]def run_service( *, routes, rules: BaseRules, swagger_definition: Optional[str] = None, middlewares: Optional[list] = None, options: Optional[Namespace] = None, dependencies: Optional[List[Union[Host, RestService, PSQLDataBase]]] = None) -> NoReturn: """ Creates and runs the service passed as ``service_class`` based on the passed ``rules`` combined with the passed ``options`` and with the ``middlewares``. If ``auth`` flag is passed as argument the service uses basic-authentication based on ``rules.auth``. Parameters: routes: The routes for the service to run. rules: The servicerules ``Rules`` from the rules.py swagger_definition: The path to the swagger_definition-file. middlewares: The middlewares to use. As default :func:`ourobori.apps.middlewares.error_middleware` is used. options: The options for the service. As default the options are build using :func:`ourobori.apps.options.build_arguments`. dependencies: List of the dependencies of the service. """ loop = asyncio.get_event_loop() loop.run_until_complete( _run_async(routes=routes, rules=rules, middlewares=middlewares, options=options, swagger_definition=swagger_definition, dependencies=dependencies)) try: loop.run_forever() except KeyboardInterrupt: print('Shutting Down!') loop.close()
async def _run_async(*, routes, rules: BaseRules, options: Optional[Namespace] = None, swagger_definition: Optional[str] = None, dependencies: Optional[List[Union[Host, RestService, PSQLDataBase]]] = None, middlewares: Optional[list] = None): """ This is only a wrapper function to be able to call :func:`run_service` without any knowledge about how to start a aiohttp-app. Parameters: routes: The routes for the service to run. rules: The servicerules ``Rules`` from the rules.py options: The options for the service. As default the options are build using :func:`ourobori.apps.options.build_arguments`. swagger_definition: The path to the swagger_definition-file. dependencies: List of the dependencies of the service. middlewares: The middlewares to use. As default :func:`ourobori.apps.middlewares.error_middleware` is used. """ if not options: options = build_arguments() options = vars(options) app = await _create_app(routes=routes, rules=rules, middlewares=middlewares, options=options, swagger_definition=swagger_definition, dependencies=dependencies) runner = web.AppRunner(app=app) await runner.setup() site = web.TCPSite(runner=runner, host=options.get('hostname') or rules.hostname, port=options.get('port') or rules.port) await site.start() __all__ = ['run_service']