Ejemplo n.º 1
0
# :copyright: (c) 2020 by Nicholas Repole and contributors.
#             See AUTHORS for more details.
# :license: MIT - See LICENSE for more details.
import apispec
from apispec.exceptions import DuplicateComponentNameError
from apispec.ext.marshmallow import MarshmallowPlugin
from contextlib import suppress
from .schemas import *
from .resources import *
from inspect import isclass
from marshmallow.fields import String
from drowsy.schema import ModelResourceSchema
from drowsy.fields import NestedRelated, APIUrl
import inflection

ma_plugin = MarshmallowPlugin()

spec = apispec.APISpec(title="Swagger Docs",
                       version="0.1.1",
                       openapi_version="3.0.2",
                       plugins=[ma_plugin])


def nestedrelated2properties(self, field, **kwargs):
    """Handle NestedRelated Drowsy field.

    Only needed due to potential use of `many=True`, whereas Nested
    fields are often embedded in List fields. This is pretty hacky,
    and basically just moves around properties that `nested2properties`
    generated.
Ejemplo n.º 2
0
def create_app(script_info=None):
 
    # instantiate the app
    app = Flask(
        __name__,
        template_folder='templates',
        static_folder='static'
    )

    # sys.modules[__name__]._current_app = app

    # set config
    app_settings = os.getenv("FLASK_APP_CONFIG", "qctrl_api.config.DevelopmentConfig")
    app.config.from_object(app_settings)

    app.config.update({
        'APISPEC_SPEC': APISpec(
            title='Q-CTRL API',
            info=qdict(
                # description=open('API.md').read(),
            ),
            basePath="/api",
            version=__version__,
            plugins=[
                # FlaskPlugin(),
                MarshmallowPlugin(),
            ],
        ),
    })

    _app_logger.info("FlaskConfig: {}".format(app.config))

    # Register custom error handler so we can see what is exactly failing at validation.
    app.errorhandler(422)(handle_unprocessable_entity)
    app.errorhandler(ValidationError)(handle_validation_error)

    # Add spec handler to app so we don't need to pass it around separately.
    app.docs = FlaskApiSpec(app)

    # set up extensions
    # login_manager.init_app(app)
    # bcrypt.init_app(app)
    toolbar.init_app(app)
    # bootstrap.init_app(app)
    db.init_app(app)
    # migrate.init_app(app, db)
    
    # CORS Plugin init
    CORS(app)

    # # flask login
    # from project.server.models import User
    # login_manager.login_view = 'user.login'
    # login_manager.login_message_category = 'danger'

    # @login_manager.user_loader
    # def load_user(user_id):
    #     return User.query.filter(User.id == int(user_id)).first()

    # # error handlers
    # @app.errorhandler(401)
    # def unauthorized_page(error):
    #     return render_template('errors/401.html'), 401

    # @app.errorhandler(403)
    # def forbidden_page(error):
    #     return render_template('errors/403.html'), 403

    # @app.errorhandler(404)
    # def page_not_found(error):
    #     return render_template('errors/404.html'), 404

    # @app.errorhandler(500)
    # def server_error_page(error):
    #     return render_template('errors/500.html'), 500

    # shell context for flask cli
    @app.shell_context_processor
    def ctx():
        return {
            "app": app,
        }

    return app
Ejemplo n.º 3
0
from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin

TESTING = True
DEBUG = True
MONGO_DBS = ['test1', 'test2']
MONGODB_SETTINGS = [{
    'alias': db,
    'db': db,
    'host': 'mongo'
} for db in MONGO_DBS]
APISPEC_SPEC = APISpec(title="My API",
                       version='v1',
                       plugins=[MarshmallowPlugin()],
                       openapi_version='2.0')
Ejemplo n.º 4
0
    def api_spec(self):
        """
        OpenApi 2.0 spec
        ---
        get:
          tags:
          - "OpenApi 2.0 spec"
          summary: "Return openapi spec
          purposes"
          description: ""
          operationId: "api_spec"
          consumes:
          - "application/json"
          produces:
          - "application/json"
          parameters:
          responses:
            200:
              description: "Success"
        """
        spec = APISpec(
            title="Channelstream API",
            version="0.7.0",
            openapi_version="2.0.0",
            plugins=(MarshmallowPlugin(), ),
        )
        spec.components.schema("ConnectBody", schema=schemas.ConnectBodySchema)
        spec.components.schema("SubscribeBody",
                               schema=schemas.SubscribeBodySchema)
        spec.components.schema("UnsubscribeBody",
                               schema=schemas.UnsubscribeBodySchema)
        spec.components.schema("UserStateBody",
                               schema=schemas.UserStateBodySchema)
        spec.components.schema("MessagesBody",
                               schema=schemas.MessageBodySchema(many=True))
        spec.components.schema("MessageBody",
                               schema=schemas.MessageBodySchema())
        spec.components.schema("MessageEditBody",
                               schema=schemas.MessageEditBodySchema(many=True))
        spec.components.schema(
            "MessagesDeleteBody",
            schema=schemas.MessagesDeleteBodySchema(many=True))
        spec.components.schema("DisconnectBody",
                               schema=schemas.DisconnectBodySchema)
        spec.components.schema("ChannelConfigBody",
                               schema=schemas.ChannelConfigSchema)
        spec.components.schema("ChannelInfoBody",
                               schema=schemas.ChannelInfoBodySchema)

        # api
        add_pyramid_paths(spec, "connect", request=self.request)
        add_pyramid_paths(spec, "subscribe", request=self.request)
        add_pyramid_paths(spec, "unsubscribe", request=self.request)
        add_pyramid_paths(spec, "user_state", request=self.request)
        add_pyramid_paths(spec, "message", request=self.request)
        add_pyramid_paths(spec, "channel_config", request=self.request)
        add_pyramid_paths(spec, "info", request=self.request)

        add_pyramid_paths(spec, "api_listen", request=self.request)
        add_pyramid_paths(spec, "api_listen_ws", request=self.request)
        add_pyramid_paths(spec, "api_disconnect", request=self.request)

        add_pyramid_paths(spec, "admin_json", request=self.request)
        spec_dict = spec.to_dict()
        spec_dict["securityDefinitions"] = {
            "APIKeyHeader": {
                "type": "apiKey",
                "name": "X-Channelstream-Secret",
                "in": "header",
            }
        }
        return spec_dict
Ejemplo n.º 5
0
# Setup API docs and Swagger
from .core import app
from .auth import add_auth_to_swagger
from apispec import APISpec
from flask_apispec.extension import FlaskApiSpec
from webargs import fields
from apispec.ext.marshmallow import MarshmallowPlugin

file_plugin = MarshmallowPlugin()
spec = APISpec(title='neuroscout',
               version='v1',
               plugins=[file_plugin],
               openapi_version='2.0')
app.config.update({'APISPEC_SPEC': spec})
add_auth_to_swagger(spec)

docs = FlaskApiSpec(app)


@file_plugin.map_to_openapi_type('file', None)
class FileField(fields.Raw):
    pass
Ejemplo n.º 6
0
    :return: The documented name for the schema
    :rtype: str
    """
    # Get an instance of the schema
    schema_instance = common.resolve_schema_instance(schema)
    if schema_instance.partial:
        prefix = "Patch-"
    elif schema_instance.only:
        prefix = "Partial-"
    else:
        prefix = ""

    # Get the class of the instance
    schema_cls = common.resolve_schema_cls(schema)
    name = prefix + schema_cls.__name__

    if name.endswith("Schema"):
        return name[:-6] or name
    return name


db = SQLAlchemy()
api = Api(
    spec_kwargs={
        'marshmallow_plugin':
        MarshmallowPlugin(schema_name_resolver=custom_name_resolver)
    })
ma = Marshmallow()
login = LoginManager()
migrate = Migrate()
def test_openapi_tools_validate_v3():
    ma_plugin = MarshmallowPlugin()
    spec = APISpec(title="Pets",
                   version="0.1",
                   plugins=(ma_plugin, ),
                   openapi_version="3.0.0")
    openapi = ma_plugin.converter

    spec.components.schema("Category", schema=CategorySchema)
    spec.components.schema("Pet", schema=PetSchema)

    spec.path(
        view=None,
        path="/category/{category_id}",
        operations={
            "get": {
                "parameters": [
                    {
                        "name": "q",
                        "in": "query",
                        "schema": {
                            "type": "string"
                        }
                    },
                    {
                        "name": "category_id",
                        "in": "path",
                        "required": True,
                        "schema": {
                            "type": "string"
                        },
                    },
                    openapi._field2parameter(
                        field=fields.List(
                            fields.Str(),
                            validate=validate.OneOf(["freddie", "roger"]),
                        ),
                        location="query",
                        name="body",
                    ),
                ] + openapi.schema2parameters(PageSchema, location="query"),
                "responses": {
                    200: {
                        "description": "success",
                        "content": {
                            "application/json": {
                                "schema": PetSchema
                            }
                        },
                    }
                },
            },
            "post": {
                "parameters": ([{
                    "name": "category_id",
                    "in": "path",
                    "required": True,
                    "schema": {
                        "type": "string"
                    },
                }]),
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": CategorySchema
                        }
                    }
                },
                "responses": {
                    201: {
                        "description": "created",
                        "content": {
                            "application/json": {
                                "schema": PetSchema
                            }
                        },
                    }
                },
            },
        },
    )
    try:
        utils.validate_spec(spec)
    except exceptions.OpenAPIError as error:
        pytest.fail(str(error))
Ejemplo n.º 8
0
    def _init_spec(self,
                   *,
                   flask_plugin=None,
                   marshmallow_plugin=None,
                   extra_plugins=None,
                   title=None,
                   version=None,
                   openapi_version=None,
                   **options):
        # Plugins
        self.flask_plugin = flask_plugin or FlaskPlugin()
        self.ma_plugin = marshmallow_plugin or MarshmallowPlugin()
        plugins = [self.flask_plugin, self.ma_plugin]
        plugins.extend(extra_plugins or ())

        # APISpec options
        title = self._app.config.get('API_TITLE', title)
        if title is None:
            raise MissingAPIParameterError(
                'API title must be specified either as "API_TITLE" '
                'app parameter or as "title" spec kwarg.')
        version = self._app.config.get('API_VERSION', version)
        if version is None:
            raise MissingAPIParameterError(
                'API version must be specified either as "API_VERSION" '
                'app parameter or as "version" spec kwarg.')
        openapi_version = self._app.config.get('OPENAPI_VERSION',
                                               openapi_version)
        if openapi_version is None:
            raise MissingAPIParameterError(
                'OpenAPI version must be specified either as "OPENAPI_VERSION '
                'app parameter or as "openapi_version" spec kwarg.')
        openapi_major_version = int(openapi_version.split('.')[0])
        if openapi_major_version < 3:
            base_path = self._app.config.get('APPLICATION_ROOT')
            options.setdefault('basePath', base_path)
            options.setdefault('produces', [
                DEFAULT_RESPONSE_CONTENT_TYPE,
            ])
            options.setdefault('consumes', [
                DEFAULT_REQUEST_BODY_CONTENT_TYPE,
            ])
        options.update(self._app.config.get('API_SPEC_OPTIONS', {}))

        # Instantiate spec
        self.spec = apispec.APISpec(
            title,
            version,
            openapi_version,
            plugins,
            **options,
        )

        # Register custom fields in spec
        for args in self._fields:
            self._register_field(*args)
        # Register custom converters in spec
        for args in self._converters:
            self._register_converter(*args)
        # Register Upload field properties function
        self.ma_plugin.converter.add_attribute_function(uploadfield2properties)

        # Register OpenAPI command group
        self._app.cli.add_command(openapi_cli)
Ejemplo n.º 9
0
    def _generate_spec(self) -> APISpec:
        """Generate the spec, return an instance of `apispec.APISpec`."""
        kwargs: dict = {}
        if self.servers:
            kwargs['servers'] = self.servers
        if self.external_docs:
            kwargs['externalDocs'] = self.external_docs

        ma_plugin: MarshmallowPlugin = MarshmallowPlugin(
            schema_name_resolver=self._schema_name_resolver)
        spec: APISpec = APISpec(title=self.title,
                                version=self.version,
                                openapi_version=self.config['OPENAPI_VERSION'],
                                plugins=[ma_plugin],
                                info=self._make_info(),
                                tags=self._make_tags(),
                                **kwargs)

        # configure flask-marshmallow URL types
        ma_plugin.converter.field_mapping[fields.URLFor] = ('string', 'url')
        ma_plugin.converter.field_mapping[fields.AbsoluteURLFor] = \
            ('string', 'url')
        if sqla is not None:  # pragma: no cover
            ma_plugin.converter.field_mapping[sqla.HyperlinkRelated] = \
                ('string', 'url')

        # security schemes
        auth_names: t.List[str] = []
        auth_schemes: t.List[HTTPAuthType] = []
        auth_blueprints: t.Dict[t.Optional[str], t.Dict[str, t.Any]] = {}

        def _update_auth_info(auth: HTTPAuthType) -> None:
            # update auth_schemes and auth_names
            auth_schemes.append(auth)
            auth_name: str = get_auth_name(auth, auth_names)
            auth_names.append(auth_name)

        # detect auth_required on before_request functions
        for blueprint_name, funcs in self.before_request_funcs.items():
            if blueprint_name is not None and \
               not self.blueprints[blueprint_name].enable_openapi:  # type: ignore
                continue
            for f in funcs:
                if hasattr(f, '_spec'):  # pragma: no cover
                    auth = f._spec.get('auth')  # type: ignore
                    if auth is not None and auth not in auth_schemes:
                        auth_blueprints[blueprint_name] = {
                            'auth': auth,
                            'roles': f._spec.get('roles')  # type: ignore
                        }
                        _update_auth_info(auth)
        # collect auth info
        for rule in self.url_map.iter_rules():
            view_func: ViewFuncType = self.view_functions[
                rule.endpoint]  # type: ignore
            if hasattr(view_func, '_spec'):
                auth = view_func._spec.get('auth')
                if auth is not None and auth not in auth_schemes:
                    _update_auth_info(auth)
            # method views
            if hasattr(view_func, '_method_spec'):
                for method_spec in view_func._method_spec.values():
                    auth = method_spec.get('auth')
                    if auth is not None and auth not in auth_schemes:
                        _update_auth_info(auth)

        security, security_schemes = get_security_and_security_schemes(
            auth_names, auth_schemes)
        for name, scheme in security_schemes.items():
            spec.components.security_scheme(name, scheme)

        # paths
        paths: t.Dict[str, t.Dict[str, t.Any]] = {}
        rules: t.List[t.Any] = sorted(list(self.url_map.iter_rules()),
                                      key=lambda rule: len(rule.rule))
        for rule in rules:
            operations: t.Dict[str, t.Any] = {}
            view_func: ViewFuncType = self.view_functions[
                rule.endpoint]  # type: ignore
            # skip endpoints from openapi blueprint and the built-in static endpoint
            if rule.endpoint.startswith('openapi') or \
               rule.endpoint.startswith('static'):
                continue
            blueprint_name: t.Optional[str] = None  # type: ignore
            if '.' in rule.endpoint:
                blueprint_name: str = rule.endpoint.rsplit(
                    '.', 1)[0]  # type: ignore
                blueprint = self.blueprints[blueprint_name]  # type: ignore
                if not hasattr(blueprint, 'enable_openapi') or \
                   not blueprint.enable_openapi:  # type: ignore
                    continue
            # add a default 200 response for bare views
            if not hasattr(view_func, '_spec'):
                if self.config['AUTO_200_RESPONSE']:
                    view_func._spec = {'response': default_response}
                else:
                    continue  # pragma: no cover
            # method views
            if hasattr(view_func, '_method_spec'):
                skip = True
                for method, method_spec in view_func._method_spec.items():
                    if method_spec.get('no_spec'):
                        if self.config['AUTO_200_RESPONSE']:
                            view_func._method_spec[method][
                                'response'] = default_response
                            skip = False
                    else:
                        skip = False
                if skip:
                    continue
            # skip views flagged with @doc(hide=True)
            if view_func._spec.get('hide'):
                continue

            # operation tags
            operation_tags: t.Optional[t.List[str]] = None
            if view_func._spec.get('tags'):
                operation_tags = view_func._spec.get('tags')
            else:
                # use blueprint name as tag
                if self.tags is None and self.config['AUTO_TAGS'] and \
                   blueprint_name is not None:
                    blueprint = self.blueprints[blueprint_name]
                    operation_tags = \
                        get_operation_tags(blueprint, blueprint_name)  # type: ignore

            for method in ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']:
                if method not in rule.methods:
                    continue
                # method views
                if hasattr(view_func, '_method_spec'):
                    if method not in view_func._method_spec:
                        continue  # pragma: no cover
                    view_func._spec = view_func._method_spec[method]

                    if view_func._spec.get('no_spec') and \
                       not self.config['AUTO_200_RESPONSE']:
                        continue
                    if view_func._spec.get('generated_summary') and \
                       not self.config['AUTO_PATH_SUMMARY']:
                        view_func._spec['summary'] = ''
                    if view_func._spec.get('generated_description') and \
                       not self.config['AUTO_PATH_DESCRIPTION']:
                        view_func._spec['description'] = ''
                    if view_func._spec.get('hide'):
                        continue
                    if view_func._spec.get('tags'):
                        operation_tags = view_func._spec.get('tags')
                    else:
                        if self.tags is None and self.config['AUTO_TAGS'] and \
                           blueprint_name is not None:
                            blueprint = self.blueprints[blueprint_name]
                            operation_tags = \
                                get_operation_tags(blueprint, blueprint_name)  # type: ignore

                operation: t.Dict[str, t.Any] = {
                    'parameters': [{
                        'in': location,
                        'schema': schema
                    } for schema, location in view_func._spec.get('args', [])],
                    'responses': {},
                }
                if operation_tags:
                    operation['tags'] = operation_tags

                # summary
                if view_func._spec.get('summary'):
                    operation['summary'] = view_func._spec.get('summary')
                else:
                    # auto-generate summary from dotstring or view function name
                    if self.config['AUTO_PATH_SUMMARY']:
                        operation['summary'] = get_path_summary(
                            view_func)  # type: ignore

                # description
                if view_func._spec.get('description'):
                    operation['description'] = view_func._spec.get(
                        'description')
                else:
                    # auto-generate description from dotstring
                    if self.config['AUTO_PATH_DESCRIPTION']:
                        docs = (view_func.__doc__ or '').strip().split('\n')
                        if len(docs) > 1:
                            # use the remain lines of docstring as description
                            operation['description'] = '\n'.join(
                                docs[1:]).strip()

                # deprecated
                if view_func._spec.get('deprecated'):
                    operation['deprecated'] = view_func._spec.get('deprecated')

                # responses
                if view_func._spec.get('response'):
                    status_code: str = str(
                        view_func._spec.get('response')['status_code'])
                    schema = view_func._spec.get('response')['schema']
                    description: str = view_func._spec.get('response')['description'] or \
                        self.config['SUCCESS_DESCRIPTION']
                    example = view_func._spec.get('response')['example']
                    examples = view_func._spec.get('response')['examples']
                    add_response(operation, status_code, schema, description,
                                 example, examples)
                else:
                    # add a default 200 response for views without using @output
                    # or @doc(responses={...})
                    if not view_func._spec.get(
                            'responses') and self.config['AUTO_200_RESPONSE']:
                        add_response(operation, '200', {},
                                     self.config['SUCCESS_DESCRIPTION'])

                # add validation error response
                if self.config['AUTO_VALIDATION_ERROR_RESPONSE'] and \
                   (view_func._spec.get('body') or view_func._spec.get('args')):
                    status_code: str = str(  # type: ignore
                        self.config['VALIDATION_ERROR_STATUS_CODE'])
                    description: str = self.config[  # type: ignore
                        'VALIDATION_ERROR_DESCRIPTION']
                    schema: SchemaType = self.config[
                        'VALIDATION_ERROR_SCHEMA']  # type: ignore
                    add_response_with_schema(spec, operation, status_code,
                                             schema, 'ValidationError',
                                             description)

                # add authentication error response
                if self.config['AUTO_AUTH_ERROR_RESPONSE'] and \
                   (view_func._spec.get('auth') or (
                       blueprint_name is not None and blueprint_name in auth_blueprints
                   )):
                    status_code: str = str(  # type: ignore
                        self.config['AUTH_ERROR_STATUS_CODE'])
                    description: str = self.config[
                        'AUTH_ERROR_DESCRIPTION']  # type: ignore
                    schema: SchemaType = self.config[
                        'HTTP_ERROR_SCHEMA']  # type: ignore
                    add_response_with_schema(spec, operation, status_code,
                                             schema, 'HTTPError', description)

                if view_func._spec.get('responses'):
                    responses: t.Union[t.List[int], t.Dict[int, str]] \
                        = view_func._spec.get('responses')
                    if isinstance(responses, list):
                        responses: t.Dict[int, str] = {}  # type: ignore
                        for status_code in view_func._spec.get('responses'):
                            responses[  # type: ignore
                                status_code] = get_reason_phrase(
                                    int(status_code), '')
                    for status_code, description in responses.items(
                    ):  # type: ignore
                        status_code: str = str(status_code)  # type: ignore
                        if status_code in operation['responses']:
                            continue
                        if status_code.startswith(
                                '4') or status_code.startswith('5'):
                            # add error response schema for error responses
                            schema: SchemaType = self.config[
                                'HTTP_ERROR_SCHEMA']  # type: ignore
                            add_response_with_schema(spec, operation,
                                                     status_code, schema,
                                                     'HTTPError', description)
                        else:
                            add_response(operation, status_code, {},
                                         description)

                # requestBody
                if view_func._spec.get('body'):
                    operation['requestBody'] = {
                        'content': {
                            'application/json': {
                                'schema': view_func._spec['body'],
                            }
                        }
                    }
                    if view_func._spec.get('body_example'):
                        example = view_func._spec.get('body_example')
                        operation['requestBody']['content'][
                            'application/json']['example'] = example
                    if view_func._spec.get('body_examples'):
                        examples = view_func._spec.get('body_examples')
                        operation['requestBody']['content'][
                            'application/json']['examples'] = examples

                # security
                # app-wide auth
                if None in auth_blueprints:
                    operation['security'] = [{
                        security[auth_blueprints[None]['auth']]:
                        auth_blueprints[None]['roles']
                    }]

                # blueprint-wide auth
                if blueprint_name is not None and blueprint_name in auth_blueprints:
                    operation['security'] = [{
                        security[auth_blueprints[blueprint_name]['auth']]:
                        auth_blueprints[blueprint_name]['roles']
                    }]

                # view-wide auth
                if view_func._spec.get('auth'):
                    operation['security'] = [{
                        security[view_func._spec['auth']]:
                        view_func._spec['roles']
                    }]

                operations[method.lower()] = operation

            # parameters
            path_arguments: t.Iterable = re.findall(r'<(([^<:]+:)?([^>]+))>',
                                                    rule.rule)
            if path_arguments:
                arguments: t.List[t.Dict[str, str]] = []
                for _, argument_type, argument_name in path_arguments:
                    argument = get_argument(argument_type, argument_name)
                    arguments.append(argument)

                for method, operation in operations.items():
                    operation[
                        'parameters'] = arguments + operation['parameters']

            path: str = re.sub(r'<([^<:]+:)?', '{',
                               rule.rule).replace('>', '}')
            if path not in paths:
                paths[path] = operations
            else:
                paths[path].update(operations)

        for path, operations in paths.items():
            # sort by method before adding them to the spec
            sorted_operations: t.Dict[str, t.Any] = {}
            for method in ['get', 'post', 'put', 'patch', 'delete']:
                if method in operations:
                    sorted_operations[method] = operations[method]
            spec.path(path=path, operations=sorted_operations)

        return spec
Ejemplo n.º 10
0
def create_app(test_config=None):
    # create the app configuration
    app = Flask(
        __name__,
        instance_path=environ.get('FLASK_APP_INSTANCE',
                                  '/user/src/app/instance'))  # instance path

    app.config.from_mapping(
        POSTGRES_HOST=environ.get('POSTGRES_HOST', ''),
        POSTGRES_USER=environ.get('POSTGRES_USER', ''),
        POSTGRES_DATABASE=environ.get('POSTGRES_DATABASE',
                                      environ.get('POSTGRES_USER', '')),
        POSTGRES_PASSWORD=environ.get('POSTGRES_PASSWORD', ''),
        SQLALCHEMY_TRACK_MODIFICATIONS=False,
        AUTH_LEEWAY=timedelta(seconds=int(environ.get(
            'AUTH_LEEWAY', '30'))),  # leeway in seconds
        BASEURL=environ.get('BASEURL', ''),
        DOCKER_HOST=environ.get('DOCKER_HOST', ''),
        DOCKER_BASEURL='http://{}'.format(environ.get('DOCKER_HOST', '')),
        TOKEN_ISSUER=environ.get('TOKEN_ISSUER',
                                 environ.get('BASEURL', 'auth')),
        ZOOKEEPER_CONNECTION_STR=environ.get('ZOOKEEPER_CONNECTION_STR',
                                             'zoo1,zoo2,zoo3'),
    )

    app.config[
        'SQLALCHEMY_DATABASE_URI'] = 'postgresql+psycopg2://{}:{}@{}/{}'.format(
            app.config['POSTGRES_USER'], app.config['POSTGRES_PASSWORD'],
            app.config['POSTGRES_HOST'], app.config['POSTGRES_DATABASE'])

    if test_config is None:
        # load the instance config if it exists, when not testing
        app.config.from_pyfile(path.join(app.instance_path, 'config.py'),
                               silent=True)
    else:
        app.config.from_mapping(test_config)

    if not app.testing:
        if app.config.get('POSTGRES_HOST') == '':
            raise Exception(
                'No postgres database host was provided. '
                'POSTGRES_HOST environment variable cannot be omitted')

        if app.config.get('POSTGRES_USER') == '':
            raise Exception(
                'No postgres database user was provided. '
                'POSTGRES_USER environment variable cannot be omitted')

        if app.config.get('POSTGRES_PASSWORD') == '':
            raise Exception(
                'No postgres database user password was provided. '
                'POSTGRES_PASSWORD environment variable cannot be omitted')

        if app.config.get('BASEURL') == '':
            raise Exception('No service base url was provided. '
                            'BASEURL environment variable cannot be omitted')

        if app.config.get('DOCKER_HOST') == '':
            raise Exception(
                'No network host within docker was provided. '
                'DOCKER_HOST environment variable cannot be omitted')

        app.config.update({
            'APISPEC_SPEC':
            APISpec(
                title='disastergram-auth',
                version='v1',
                openapi_version='2.0',
                plugins=[MarshmallowPlugin()],
            ),
            'APISPEC_SWAGGER_URL':
            '/auth/spec',
            'APISPEC_SWAGGER_UI_URL':
            '/auth/spec-ui',
        })

        # Only do zookeeper for non testing configs for now
        znode_data = {
            'TOKEN_ISSUER': app.config['TOKEN_ISSUER'],
            'BASEURL': app.config['BASEURL'],
            'DOCKER_HOST': app.config['DOCKER_HOST'],
            'DOCKER_BASEURL': app.config['DOCKER_BASEURL'],
            'PUBLIC_KEY': app.config['PUBLIC_KEY'].decode('utf-8')
        }

        global zk
        zk = AuthZoo(
            KazooClient(app.config['ZOOKEEPER_CONNECTION_STR'],
                        connection_retry=KazooRetry(max_tries=-1),
                        logger=app.logger), znode_data)

    # INIT

    db.init_app(app)
    mi.init_app(app,
                db,
                directory=environ.get('FLASK_APP_MIGRATIONS', 'migrations'))
    ma.init_app(app)
    bc.init_app(app)

    if not app.testing:
        docs.init_app(app)

    # for some reason when not in development
    # this call fails ¯\_(ツ)_/¯.
    # Probably some kind of problem with
    # threading and prefork.
    if app.env == 'development':
        from auth import models
        models.init_db(app)

    from auth import service

    app.register_blueprint(service.bp)

    if not app.testing:
        docs.register(service.user_register, blueprint='auth')

        docs.register(service.user_read, blueprint='auth')
        docs.register(service.user_replace, blueprint='auth')
        docs.register(service.user_update, blueprint='auth')
        docs.register(service.user_del, blueprint='auth')

        docs.register(service.user_read_id, blueprint='auth')
        docs.register(service.user_replace_id, blueprint='auth')
        docs.register(service.user_update_id, blueprint='auth')
        docs.register(service.user_del_id, blueprint='auth')

        docs.register(service.login, blueprint='auth')
        docs.register(service.refresh_token, blueprint='auth')
        docs.register(service.logout, blueprint='auth')

        docs.register(service.pub_key, blueprint='auth')

    return app
Ejemplo n.º 11
0
def test_openapi_tools_validate_v3():
    ma_plugin = MarshmallowPlugin()
    spec = APISpec(
        title='Pets',
        version='0.1',
        plugins=(ma_plugin, ),
        openapi_version='3.0.0',
    )
    openapi = ma_plugin.openapi

    spec.components.schema('Category', schema=CategorySchema)
    spec.components.schema('Pet', schema=PetSchemaV3)

    spec.path(
        view=None,
        path='/category/{category_id}',
        operations={
            'get': {
                'parameters': [
                    {
                        'name': 'q',
                        'in': 'query',
                        'schema': {
                            'type': 'string'
                        },
                    },
                    {
                        'name': 'category_id',
                        'in': 'path',
                        'required': True,
                        'schema': {
                            'type': 'string'
                        },
                    },
                    openapi.field2parameter(
                        field=fields.List(
                            fields.Str(),
                            validate=validate.OneOf(['freddie', 'roger']),
                            location='querystring',
                        ),
                        name='body',
                        use_refs=False,
                    ),
                ] + openapi.schema2parameters(PageSchema, default_in='query'),
                'responses': {
                    200: {
                        'description': 'success',
                        'content': {
                            'application/json': {
                                'schema': PetSchemaV3,
                            },
                        },
                    },
                },
            },
            'post': {
                'parameters': ([
                    {
                        'name': 'category_id',
                        'in': 'path',
                        'required': True,
                        'schema': {
                            'type': 'string'
                        },
                    },
                ]),
                'requestBody': {
                    'content': {
                        'application/json': {
                            'schema': CategorySchema,
                        },
                    },
                },
                'responses': {
                    201: {
                        'description': 'created',
                        'content': {
                            'application/json': {
                                'schema': PetSchemaV3,
                            },
                        },
                    },
                },
            },
        },
    )
    try:
        utils.validate_spec(spec)
    except exceptions.OpenAPIError as error:
        pytest.fail(str(error))
Ejemplo n.º 12
0
from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin


def resolver(schema):
    return None


spec = APISpec(
    title="events_api",
    version="1.0",
    openapi_version="2.0",
    plugins=[MarshmallowPlugin(schema_name_resolver=resolver)],
)

ma_plugin = spec.plugins.pop()


def as_model(ns, schema):
    response_schema = ma_plugin.resolver.resolve_schema_dict(schema)
    return ns.schema_model(schema.__name__, response_schema)
Ejemplo n.º 13
0
from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin
from sanicplugin import SanicPlugin
from app.api.v1.views.cart_view import UserView
from app.api.v1.models.cart import UserSchema

# @TODO: Add tests in CI for successful generation of APISpec file
# Create spec object
spec = APISpec(
    title='StarScream',
    version='1.0.0',
    openapi_version='2.0',
    info=dict(
        description='Check status by pinging the server'
    ),
    plugins=[MarshmallowPlugin(), SanicPlugin()]
)

# Add tags
spec.add_tag(
    {"name": "user", "description": "Everything you can do with a user"})
# Add definitions
spec.definition('User', schema=UserSchema)
# Add views to generate paths for
user_view = UserView.as_view('user')
spec.add_path(path='/user', view=user_view)

# Save this generated spec to a file for now.
with open('swagger.json', 'w') as f:
    json.dump(spec.to_dict(), f)
Ejemplo n.º 14
0
def openapi_format(format="yaml",
                   server="localhost",
                   no_servers=False,
                   return_tags=False):
    extra_specs = {
        'info': {
            'description':
            'The Faraday REST API enables you to interact with '
            '[our server](https://github.com/infobyte/faraday).\n'
            'Use this API to interact or integrate with Faraday'
            ' server. This page documents the REST API, with HTTP'
            ' response codes and example requests and responses.'
        },
        'security': {
            "ApiKeyAuth": []
        }
    }

    if not no_servers:
        extra_specs['servers'] = [{'url': f'https://{server}/_api'}]

    spec = APISpec(
        title="Faraday API",
        version="2",
        openapi_version="3.0.2",
        plugins=[FaradayAPIPlugin(),
                 FlaskPlugin(),
                 MarshmallowPlugin()],
        **extra_specs)
    api_key_scheme = {
        "type": "apiKey",
        "in": "header",
        "name": "Authorization"
    }

    spec.components.security_scheme("API_KEY", api_key_scheme)
    response_401_unauthorized = {
        "description":
        "You are not authenticated or your API key is missing "
        "or invalid"
    }
    spec.components.response("UnauthorizedError", response_401_unauthorized)

    tags = set()

    with app.test_request_context():
        for endpoint in app.view_functions.values():
            spec.path(view=endpoint, app=app)

        # Set up global tags
        spec_yaml = yaml.load(spec.to_yaml(), Loader=yaml.SafeLoader)
        for path_value in spec_yaml["paths"].values():
            for data_value in path_value.values():
                if 'tags' in data_value and any(data_value['tags']):
                    for tag in data_value['tags']:
                        tags.add(tag)
        for tag in sorted(tags):
            spec.tag({'name': tag})

        if return_tags:
            return sorted(tags)

        if format.lower() == "yaml":
            print(spec.to_yaml())
        else:
            print(json.dumps(spec.to_dict(), indent=2))
Ejemplo n.º 15
0
def spec_from_handlers(handlers, exclude_internal=True, metadata=None):
    """Generate an OpenAPI spec from Tornado handlers.

    The docstrings of the various http methods of the Tornado handlers
    (`get`, `put`, etc.), should contain OpenAPI yaml after three
    dashed.  E.g.:

    ```yaml
    ---
    description: Retrieve a source
    parameters:
      - in: path
        name: obj_id
        required: false
        schema:
          type: integer
          required: false
    responses:
      200:
        content:
          application/json:
            schema:
              oneOf:
                - SingleSource
                - Error
    ```

    The yaml snippet may contain two top-level keywords, `single` and
    `multiple`, that can be used to disambiguate the OpenAPI spec for
    a single URL that is meant to return both single and multiple
    objects.  E.g., `/api/sources/{obj_id}` may return multiple
    objects if `{obj_id}` is left unspecified.  If these keywords
    are not specified, the OpenAPI snippet is used as is.

    Schemas are automatically resolved to matching Marshmallow objects
    in the `spec` module.  E.g., in the above example we use
    `SingleSource` and `Error`, which refer to `spec.SingleSource` and
    `spec.Error`.  All schemas in `schema` are added to the OpenAPI definition.

    """
    meta = {
        'title': 'SkyPortal',
        'version': __version__,
        'openapi_version': '3.0.2',
        'info': {
            'description': open(api_description, 'r').read(),
            'x-logo': {
                'url':
                'https://raw.githubusercontent.com/skyportal/skyportal/master/static/images/skyportal_logo.png',
                'backgroundColor': '#FFFFFF',
                'altText': 'SkyPortal logo',
                'href': 'https://skyportal.io/docs',
            },
        },
    }
    if metadata is not None:
        meta.update(metadata)

    openapi_spec = APISpec(
        **meta,
        plugins=[MarshmallowPlugin()],
    )

    token_scheme = {
        "type":
        "apiKey",
        "in":
        "header",
        "name":
        "Authorization",
        "description":
        "Header should be in the format 'token abcd-efgh-0000-1234'",
    }
    openapi_spec.components.security_scheme("token", token_scheme)

    schema.register_components(openapi_spec)
    from apispec import yaml_utils
    import inspect
    import re

    HTTP_METHODS = ("get", "put", "post", "delete", "options", "head", "patch")
    handlers = [
        handler for handler in handlers
        if not isinstance(handler, URLSpec) and len(handler) == 2
    ]
    if exclude_internal:
        handlers = [(route, handler_cls) for (route, handler_cls) in handlers
                    if '/internal/' not in route]
    for (endpoint, handler) in handlers:
        for http_method in HTTP_METHODS:
            method = getattr(handler, http_method)
            if method.__doc__ is None:
                continue

            path_template = endpoint
            path_template = re.sub(r'\(.*?\)\??', '/{}', path_template)
            path_template = re.sub(r'(/)+', '/', path_template)
            path_parameters = path_template.count('{}')

            spec = yaml_utils.load_yaml_from_docstring(method.__doc__)
            parameters = list(inspect.signature(method).parameters.keys())[1:]
            parameters = parameters + (path_parameters - len(parameters)) * [
                '',
            ]

            if parameters[-1:] == [''] and path_template.endswith('/{}'):
                path_template = path_template[:-3]

            multiple_spec = spec.pop('multiple', {})
            single_spec = spec.pop('single', {})
            other_spec = spec

            for subspec in [single_spec, other_spec]:
                if subspec:
                    path = path_template.format(*parameters)
                    openapi_spec.path(path=path,
                                      operations={http_method: subspec})

            if multiple_spec:
                multiple_path_template = path_template.rsplit('/', 1)[0]
                multiple_path = multiple_path_template.format(*parameters[:-1])
                openapi_spec.path(path=multiple_path,
                                  operations={http_method: multiple_spec})

    return openapi_spec
Ejemplo n.º 16
0
def test_openapi_tools_validate_v2():
    ma_plugin = MarshmallowPlugin()
    spec = APISpec(
        title='Pets',
        version='0.1',
        plugins=(ma_plugin, ),
        openapi_version='2.0',
    )
    openapi = ma_plugin.openapi

    spec.definition('Category', schema=CategorySchema)
    spec.definition('Pet',
                    schema=PetSchema,
                    extra_fields={'discriminator': 'name'})

    spec.add_path(
        view=None,
        path='/category/{category_id}',
        operations={
            'get': {
                'parameters': [
                    {
                        'name': 'q',
                        'in': 'query',
                        'type': 'string'
                    },
                    {
                        'name': 'category_id',
                        'in': 'path',
                        'required': True,
                        'type': 'string'
                    },
                    openapi.field2parameter(
                        field=fields.List(
                            fields.Str(),
                            validate=validate.OneOf(['freddie', 'roger']),
                            location='querystring',
                        ),
                        name='body',
                        use_refs=False,
                    ),
                ] + openapi.schema2parameters(PageSchema, default_in='query'),
                'responses': {
                    200: {
                        'schema': PetSchema,
                        'description': 'A pet',
                    },
                },
            },
            'post': {
                'parameters': ([{
                    'name': 'category_id',
                    'in': 'path',
                    'required': True,
                    'type': 'string'
                }] + openapi.schema2parameters(CategorySchema,
                                               default_in='body')),
                'responses': {
                    201: {
                        'schema': PetSchema,
                        'description': 'A pet',
                    },
                },
            },
        },
    )
    try:
        utils.validate_spec(spec)
    except exceptions.OpenAPIError as error:
        pytest.fail(str(error))
Ejemplo n.º 17
0
def create_app(
    name: str = __name__,
    mode: ServerModes = ServerModes.NORMAL,
    options: Optional[Dict[str, bool]] = None,
) -> Flask:
    """Create the server istance for Flask application"""

    if PRODUCTION and TESTING and not FORCE_PRODUCTION_TESTS:  # pragma: no cover
        print_and_exit("Unable to execute tests in production")

    # TERM is not catched by Flask
    # https://github.com/docker/compose/issues/4199#issuecomment-426109482
    # signal.signal(signal.SIGTERM, teardown_handler)
    # SIGINT is registered as STOPSIGNAL in Dockerfile
    signal.signal(signal.SIGINT, teardown_handler)

    # Flask app instance
    # template_folder = template dir for output in HTML
    flask_app = Flask(name, template_folder=str(ABS_RESTAPI_PATH.joinpath("templates")))

    # CORS
    if not PRODUCTION:

        if TESTING:
            cors_origin = "*"
        else:  # pragma: no cover
            cors_origin = get_frontend_url()
            # Beware, this only works because get_frontend_url never append a port
            cors_origin += ":*"

        CORS(
            flask_app,
            allow_headers=[
                "Content-Type",
                "Authorization",
                "X-Requested-With",
                "x-upload-content-length",
                "x-upload-content-type",
                "content-range",
            ],
            supports_credentials=["true"],
            methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
            resources={r"*": {"origins": cors_origin}},
        )

        log.debug("CORS Enabled")

    # Flask configuration from config file
    flask_app.config.from_object(config)
    flask_app.json_encoder = ExtendedJSONEncoder

    # Used to force flask to avoid json sorting and ensure that
    # the output to reflect the order of field in the Marshmallow schema
    flask_app.config["JSON_SORT_KEYS"] = False

    log.debug("Flask app configured")

    if PRODUCTION:
        log.info("Production server mode is ON")

    endpoints_loader = EndpointsLoader()

    if HOST_TYPE == DOCS:  # pragma: no cover
        log.critical("Creating mocked configuration")
        mem.configuration = {}

        log.critical("Loading Mocked Initializer and Customizer classes")
        from restapi.mocks import Customizer, Initializer

        mem.initializer = Initializer
        mem.customizer = Customizer()

    else:

        mem.configuration = endpoints_loader.load_configuration()
        mem.initializer = Meta.get_class("initialization", "Initializer")
        if not mem.initializer:  # pragma: no cover
            print_and_exit("Invalid Initializer class")

        customizer = Meta.get_class("customization", "Customizer")
        if not customizer:  # pragma: no cover
            print_and_exit("Invalid Customizer class")
        mem.customizer = customizer()

    if not isinstance(mem.customizer, BaseCustomizer):  # pragma: no cover
        print_and_exit("Invalid Customizer class, it should inherit BaseCustomizer")

    Connector.init_app(app=flask_app, worker_mode=(mode == ServerModes.WORKER))

    # Initialize reading of all files
    mem.geo_reader = geolite2.reader()
    # when to close??
    # geolite2.close()

    if mode == ServerModes.INIT:
        Connector.project_init(options=options)

    if mode == ServerModes.DESTROY:
        Connector.project_clean()

    # Restful plugin with endpoint mapping (skipped in INIT|DESTROY|WORKER modes)
    if mode == ServerModes.NORMAL:

        logging.getLogger("werkzeug").setLevel(logging.ERROR)

        # warnings levels:
        # default  # Warn once per call location
        # error    # Convert to exceptions
        # always   # Warn every time
        # module   # Warn once per calling module
        # once     # Warn once per Python process
        # ignore   # Never warn

        # Types of warnings:
        # Warning: This is the base class of all warning category classes
        # UserWarning: The default category for warn().
        # DeprecationWarning: Base category for warnings about deprecated features when
        #                     those warnings are intended for other Python developers
        # SyntaxWarning: Base category for warnings about dubious syntactic features.
        # RuntimeWarning: Base category for warnings about dubious runtime features.
        # FutureWarning: Base category for warnings about deprecated features when those
        #                warnings are intended for end users
        # PendingDeprecationWarning: Base category for warnings about features that will
        #                            be deprecated in the future (ignored by default).
        # ImportWarning: Base category for warnings triggered during the process of
        #                importing a module
        # UnicodeWarning: Base category for warnings related to Unicode.
        # BytesWarning: Base category for warnings related to bytes and bytearray.
        # ResourceWarning: Base category for warnings related to resource usage

        if TESTING:
            warnings.simplefilter("always", Warning)
            warnings.simplefilter("error", UserWarning)
            warnings.simplefilter("error", DeprecationWarning)
            warnings.simplefilter("error", SyntaxWarning)
            warnings.simplefilter("error", RuntimeWarning)
            warnings.simplefilter("error", FutureWarning)
            # warnings about features that will be deprecated in the future
            warnings.simplefilter("default", PendingDeprecationWarning)
            warnings.simplefilter("error", ImportWarning)
            warnings.simplefilter("error", UnicodeWarning)
            warnings.simplefilter("error", BytesWarning)
            # Can't set this an error due to false positives with downloads
            # a lot of issues like: https://github.com/pallets/flask/issues/2468
            warnings.simplefilter("always", ResourceWarning)
            warnings.simplefilter("default", Neo4jExperimentalWarning)

            # Remove me in a near future, this is due to hypothesis with pytest 7
            # https://github.com/HypothesisWorks/hypothesis/issues/3222
            warnings.filterwarnings(
                "ignore", message="A private pytest class or function was used."
            )

        elif PRODUCTION:  # pragma: no cover
            warnings.simplefilter("ignore", Warning)
            warnings.simplefilter("always", UserWarning)
            warnings.simplefilter("default", DeprecationWarning)
            warnings.simplefilter("ignore", SyntaxWarning)
            warnings.simplefilter("ignore", RuntimeWarning)
            warnings.simplefilter("ignore", FutureWarning)
            warnings.simplefilter("ignore", PendingDeprecationWarning)
            warnings.simplefilter("ignore", ImportWarning)
            warnings.simplefilter("ignore", UnicodeWarning)
            warnings.simplefilter("ignore", BytesWarning)
            warnings.simplefilter("ignore", ResourceWarning)
            # even if ignore it is raised once
            # because of the imports executed before setting this to ignore
            warnings.simplefilter("ignore", Neo4jExperimentalWarning)
        else:  # pragma: no cover
            warnings.simplefilter("default", Warning)
            warnings.simplefilter("always", UserWarning)
            warnings.simplefilter("always", DeprecationWarning)
            warnings.simplefilter("default", SyntaxWarning)
            warnings.simplefilter("default", RuntimeWarning)
            warnings.simplefilter("always", FutureWarning)
            warnings.simplefilter("default", PendingDeprecationWarning)
            warnings.simplefilter("default", ImportWarning)
            warnings.simplefilter("default", UnicodeWarning)
            warnings.simplefilter("default", BytesWarning)
            warnings.simplefilter("always", ResourceWarning)
            # even if ignore it is raised once
            # because of the imports executed before setting this to ignore
            warnings.simplefilter("ignore", Neo4jExperimentalWarning)

        # ignore warning messages from apispec
        warnings.filterwarnings(
            "ignore", message="Multiple schemas resolved to the name "
        )

        # ignore warning messages on flask socket after teardown
        warnings.filterwarnings("ignore", message="unclosed <socket.socket")

        # from flask_caching 1.10.1 with python 3.10 on core tests...
        # try to remove this once upgraded flask_caching in a near future
        warnings.filterwarnings(
            "ignore",
            message="_SixMetaPathImporter.find_spec",
        )

        # Raised from sentry_sdk 1.5.11 with python 3.10 events
        warnings.filterwarnings(
            "ignore",
            message="SelectableGroups dict interface is deprecated. Use select.",
        )

        mem.cache = Cache.get_instance(flask_app)

        endpoints_loader.load_endpoints()
        mem.authenticated_endpoints = endpoints_loader.authenticated_endpoints
        mem.private_endpoints = endpoints_loader.private_endpoints

        for endpoint in endpoints_loader.endpoints:
            ename = endpoint.cls.__name__.lower()
            endpoint_view = endpoint.cls.as_view(ename)
            for url in endpoint.uris:
                flask_app.add_url_rule(url, view_func=endpoint_view)

        # APISpec configuration
        api_url = get_backend_url()
        scheme, host = api_url.rstrip("/").split("://")

        spec = APISpec(
            title=get_project_configuration(
                "project.title", default="Your application name"
            ),
            version=get_project_configuration("project.version", default="0.0.1"),
            openapi_version="2.0",
            # OpenApi 3 not working with FlaskApiSpec
            # -> Duplicate parameter with name body and location body
            # https://github.com/jmcarp/flask-apispec/issues/170
            # Find other warning like this by searching:
            # **FASTAPI**
            # openapi_version="3.0.2",
            plugins=[MarshmallowPlugin()],
            host=host,
            schemes=[scheme],
            tags=endpoints_loader.tags,
        )
        # OpenAPI 3 changed the definition of the security level.
        # Some changes needed here?

        if Env.get_bool("AUTH_ENABLE"):
            api_key_scheme = {"type": "apiKey", "in": "header", "name": "Authorization"}
            spec.components.security_scheme("Bearer", api_key_scheme)

        flask_app.config.update(
            {
                "APISPEC_SPEC": spec,
                # 'APISPEC_SWAGGER_URL': '/api/swagger',
                "APISPEC_SWAGGER_URL": None,
                # 'APISPEC_SWAGGER_UI_URL': '/api/swagger-ui',
                # Disable Swagger-UI
                "APISPEC_SWAGGER_UI_URL": None,
            }
        )

        mem.docs = FlaskApiSpec(flask_app)

        # Clean app routes
        ignore_verbs = {"HEAD", "OPTIONS"}

        for rule in flask_app.url_map.iter_rules():

            view_function = flask_app.view_functions[rule.endpoint]
            if not hasattr(view_function, "view_class"):
                continue

            newmethods = ignore_verbs.copy()
            rulename = str(rule)

            if rule.methods:
                for verb in rule.methods - ignore_verbs:
                    method = verb.lower()
                    if method in endpoints_loader.uri2methods[rulename]:
                        # remove from flask mapping
                        # to allow 405 response
                        newmethods.add(verb)

            rule.methods = newmethods

        # Register swagger. Note: after method mapping cleaning
        with flask_app.app_context():
            for endpoint in endpoints_loader.endpoints:
                try:
                    mem.docs.register(endpoint.cls)
                except TypeError as e:  # pragma: no cover
                    print(e)
                    log.error("Cannot register {}: {}", endpoint.cls.__name__, e)

    # marshmallow errors handler
    # Can't get the typing to work with flask 2.1
    flask_app.register_error_handler(422, handle_marshmallow_errors)  # type: ignore
    flask_app.register_error_handler(400, handle_http_errors)  # type: ignore
    flask_app.register_error_handler(404, handle_http_errors)  # type: ignore
    flask_app.register_error_handler(405, handle_http_errors)  # type: ignore
    flask_app.register_error_handler(500, handle_http_errors)  # type: ignore

    # flask_app.before_request(inspect_request)
    # Logging responses
    # Can't get the typing to work with flask 2.1
    flask_app.after_request(handle_response)  # type: ignore

    if SENTRY_URL is not None:  # pragma: no cover

        if PRODUCTION:
            sentry_sdk_init(
                dsn=SENTRY_URL,
                # already catched by handle_marshmallow_errors
                ignore_errors=[werkzeug.exceptions.UnprocessableEntity],
                integrations=[FlaskIntegration()],
            )
            log.info("Enabled Sentry {}", SENTRY_URL)
        else:
            # Could be enabled in print mode
            # sentry_sdk_init(transport=print)
            log.info("Skipping Sentry, only enabled in PRODUCTION mode")

    log.info("Boot completed")
    if PRODUCTION and not TESTING and name == MAIN_SERVER_NAME:  # pragma: no cover
        save_event_log(
            event=Events.server_startup,
            payload={"server": name},
            user=None,
            target=None,
        )

    return flask_app
Ejemplo n.º 18
0
def test_openapi_tools_validate_v3():
    ma_plugin = MarshmallowPlugin()
    spec = APISpec(
        title='Pets',
        version='0.1',
        plugins=(ma_plugin, ),
        openapi_version='3.0.0',
    )
    #openapi = ma_plugin.openapi

    spec.definition('Category', schema=CategorySchema)
    spec.definition('Pet', schema=PetSchemaV3)

    spec.add_path(
        view=None,
        path='/category/{category_id}',
        operations={
            'get': {
                'parameters': [
                    {
                        'name': 'q',
                        'in': 'query',
                        'schema': {
                            'type': 'string'
                        },
                    },
                    {
                        'name': 'category_id',
                        'in': 'path',
                        'required': True,
                        'schema': {
                            'type': 'string'
                        },
                    },
                ],  # + openapi.schema2parameters(PageSchema, default_in='query'),
                'responses': {
                    200: {
                        'description': 'success',
                        'content': {
                            'application/json': {
                                'schema': PetSchemaV3,
                            },
                        },
                    },
                },
            },
            'post': {
                'parameters': ([
                    {
                        'name': 'category_id',
                        'in': 'path',
                        'required': True,
                        'schema': {
                            'type': 'string'
                        },
                    },
                ]),
                'requestBody': {
                    'content': {
                        'application/json': {
                            'schema': CategorySchema,
                        },
                    },
                },
                'responses': {
                    201: {
                        'description': 'created',
                        'content': {
                            'application/json': {
                                'schema': PetSchemaV3,
                            },
                        },
                    },
                },
            },
        },
    )
    try:
        utils.validate_spec(spec)
    except exceptions.OpenAPIError as error:
        pytest.fail(str(error))
Ejemplo n.º 19
0
def create_app(
    name: str = __name__,
    mode: ServerModes = ServerModes.NORMAL,
    options: Optional[Dict[str, bool]] = None,
) -> Flask:
    """ Create the server istance for Flask application """

    if PRODUCTION and TESTING and not FORCE_PRODUCTION_TESTS:  # pragma: no cover
        print_and_exit("Unable to execute tests in production")

    # TERM is not catched by Flask
    # https://github.com/docker/compose/issues/4199#issuecomment-426109482
    # signal.signal(signal.SIGTERM, teardown_handler)
    # SIGINT is registered as STOPSIGNAL in Dockerfile
    signal.signal(signal.SIGINT, teardown_handler)

    # Flask app instance
    # template_folder = template dir for output in HTML
    microservice = Flask(
        name, template_folder=os.path.join(ABS_RESTAPI_PATH, "templates")
    )

    # CORS
    if not PRODUCTION:
        cors = CORS(
            allow_headers=[
                "Content-Type",
                "Authorization",
                "X-Requested-With",
                "x-upload-content-length",
                "x-upload-content-type",
                "content-range",
            ],
            supports_credentials=["true"],
            methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
        )

        cors.init_app(microservice)
        log.debug("CORS Injected")

    # Flask configuration from config file
    microservice.config.from_object(config)
    log.debug("Flask app configured")

    if PRODUCTION:
        log.info("Production server mode is ON")

    endpoints_loader = EndpointsLoader()
    mem.configuration = endpoints_loader.load_configuration()

    mem.initializer = Meta.get_class("initialization", "Initializer")
    if not mem.initializer:  # pragma: no cover
        print_and_exit("Invalid Initializer class")

    mem.customizer = Meta.get_instance("customization", "Customizer")
    if not mem.customizer:  # pragma: no cover
        print_and_exit("Invalid Customizer class")

    if not isinstance(mem.customizer, BaseCustomizer):  # pragma: no cover
        print_and_exit("Invalid Customizer class, it should inherit BaseCustomizer")

    Connector.init_app(app=microservice, worker_mode=(mode == ServerModes.WORKER))

    # Initialize reading of all files
    mem.geo_reader = geolite2.reader()
    # when to close??
    # geolite2.close()

    if mode == ServerModes.INIT:
        Connector.project_init(options=options)

    if mode == ServerModes.DESTROY:
        Connector.project_clean()

    # Restful plugin with endpoint mapping (skipped in INIT|DESTROY|WORKER modes)
    if mode == ServerModes.NORMAL:

        logging.getLogger("werkzeug").setLevel(logging.ERROR)
        # ignore warning messages from apispec
        warnings.filterwarnings(
            "ignore", message="Multiple schemas resolved to the name "
        )
        mem.cache = Cache.get_instance(microservice)

        endpoints_loader.load_endpoints()
        mem.authenticated_endpoints = endpoints_loader.authenticated_endpoints
        mem.private_endpoints = endpoints_loader.private_endpoints

        # Triggering automatic mapping of REST endpoints
        rest_api = Api(catch_all_404s=True)

        for endpoint in endpoints_loader.endpoints:
            # Create the restful resource with it;
            # this method is from RESTful plugin
            rest_api.add_resource(endpoint.cls, *endpoint.uris)

        # HERE all endpoints will be registered by using FlaskRestful
        rest_api.init_app(microservice)

        # APISpec configuration
        api_url = get_backend_url()
        scheme, host = api_url.rstrip("/").split("://")

        spec = APISpec(
            title=get_project_configuration(
                "project.title", default="Your application name"
            ),
            version=get_project_configuration("project.version", default="0.0.1"),
            openapi_version="2.0",
            # OpenApi 3 not working with FlaskApiSpec
            # -> Duplicate parameter with name body and location body
            # https://github.com/jmcarp/flask-apispec/issues/170
            # Find other warning like this by searching:
            # **FASTAPI**
            # openapi_version="3.0.2",
            plugins=[MarshmallowPlugin()],
            host=host,
            schemes=[scheme],
            tags=endpoints_loader.tags,
        )
        # OpenAPI 3 changed the definition of the security level.
        # Some changes needed here?
        api_key_scheme = {"type": "apiKey", "in": "header", "name": "Authorization"}
        spec.components.security_scheme("Bearer", api_key_scheme)

        microservice.config.update(
            {
                "APISPEC_SPEC": spec,
                # 'APISPEC_SWAGGER_URL': '/api/swagger',
                "APISPEC_SWAGGER_URL": None,
                # 'APISPEC_SWAGGER_UI_URL': '/api/swagger-ui',
                # Disable Swagger-UI
                "APISPEC_SWAGGER_UI_URL": None,
            }
        )

        mem.docs = FlaskApiSpec(microservice)

        # Clean app routes
        ignore_verbs = {"HEAD", "OPTIONS"}

        for rule in microservice.url_map.iter_rules():

            endpoint = microservice.view_functions[rule.endpoint]
            if not hasattr(endpoint, "view_class"):
                continue

            newmethods = ignore_verbs.copy()
            rulename = str(rule)

            for verb in rule.methods - ignore_verbs:
                method = verb.lower()
                if method in endpoints_loader.uri2methods[rulename]:
                    # remove from flask mapping
                    # to allow 405 response
                    newmethods.add(verb)

            rule.methods = newmethods

        # Register swagger. Note: after method mapping cleaning
        with microservice.app_context():
            for endpoint in endpoints_loader.endpoints:
                try:
                    mem.docs.register(endpoint.cls)
                except TypeError as e:  # pragma: no cover
                    print(e)
                    log.error("Cannot register {}: {}", endpoint.cls.__name__, e)

    # marshmallow errors handler
    microservice.register_error_handler(422, handle_marshmallow_errors)

    # Logging responses
    microservice.after_request(handle_response)

    if SENTRY_URL is not None:  # pragma: no cover

        if PRODUCTION:
            sentry_sdk.init(
                dsn=SENTRY_URL,
                # already catched by handle_marshmallow_errors
                ignore_errors=[werkzeug.exceptions.UnprocessableEntity],
                integrations=[FlaskIntegration()],
            )
            log.info("Enabled Sentry {}", SENTRY_URL)
        else:
            # Could be enabled in print mode
            # sentry_sdk.init(transport=print)
            log.info("Skipping Sentry, only enabled in PRODUCTION mode")

    log.info("Boot completed")

    return microservice
Ejemplo n.º 20
0
def setup_app(app):
    """Setup the app."""

    # set app default logging to INFO
    app.logger.setLevel(logging.INFO)
    logging.getLogger('o365_notifications').addHandler(
        flask_logging.default_handler)
    logging.getLogger('o365_notifications').setLevel(logging.INFO)

    # link db to app
    db.init_app(app)

    # link api to app
    api.init_app(app)

    # link cache to app
    cache.init_app(app)

    with app.app_context():

        # create tables if they do not exist already
        db.create_all()

        # make use of OAS3 schema converters
        openapi3_converters()

        # use app context to load namespaces, blueprints and schemas
        import src.resources.tickets
        from src.serialization.serializers.HttpError import HttpErrorSchema
        from src.serialization.serializers.jira.Issue import IssueSchema

    # initialize root blueprint
    bp = Blueprint('api',
                   __name__,
                   url_prefix=app.config['APPLICATION_CONTEXT'])

    # link api to blueprint
    api.init_app(bp)

    # register blueprints
    app.register_blueprint(bp)

    # link swagger to app
    swagger.init_app(app)

    # Redirect incomplete paths to app context root
    app.add_url_rule('/', 'index',
                     lambda: redirect(url_for('flasgger.apidocs')))
    subs = app.config['APPLICATION_CONTEXT'].split('/')
    for n in range(2, len(subs)):
        rule = '/'.join(subs[:n] + ['/'])
        app.add_url_rule(rule, ''.join(('index-', str(n - 1))),
                         lambda: redirect(url_for('flasgger.apidocs')))

    # define OAS3 base template
    swagger.template = flasgger.apispec_to_template(
        app=app,
        spec=APISpec(title=app.config['OPENAPI_SPEC']['info']['title'],
                     version=app.config['OPENAPI_SPEC']['info']['version'],
                     openapi_version=app.config['OPENAPI_SPEC']['openapi'],
                     plugins=(MarshmallowPlugin(), ),
                     basePath=app.config['APPLICATION_CONTEXT'],
                     **app.config['OPENAPI_SPEC']),
        definitions=[HttpErrorSchema, IssueSchema])

    # register cli commands
    app.cli.add_command(o365_cli)
Ejemplo n.º 21
0
def marshmallow_plugin():
    return MarshmallowPlugin()
Ejemplo n.º 22
0
def test_openapi_tools_validate_v2():
    ma_plugin = MarshmallowPlugin()
    spec = APISpec(title="Pets",
                   version="0.1",
                   plugins=(ma_plugin, ),
                   openapi_version="2.0")
    openapi = ma_plugin.openapi

    spec.components.schema("Category", schema=CategorySchema)
    spec.components.schema("Pet", {"discriminator": "name"}, schema=PetSchema)

    spec.path(
        view=None,
        path="/category/{category_id}",
        operations={
            "get": {
                "parameters": [
                    {
                        "name": "q",
                        "in": "query",
                        "type": "string"
                    },
                    {
                        "name": "category_id",
                        "in": "path",
                        "required": True,
                        "type": "string",
                    },
                    openapi.field2parameter(
                        field=fields.List(
                            fields.Str(),
                            validate=validate.OneOf(["freddie", "roger"]),
                            location="querystring",
                        ),
                        name="body",
                    ),
                ] + openapi.schema2parameters(PageSchema, default_in="query"),
                "responses": {
                    200: {
                        "schema": PetSchema,
                        "description": "A pet"
                    }
                },
            },
            "post": {
                "parameters": ([{
                    "name": "category_id",
                    "in": "path",
                    "required": True,
                    "type": "string",
                }] + openapi.schema2parameters(CategorySchema,
                                               default_in="body")),
                "responses": {
                    201: {
                        "schema": PetSchema,
                        "description": "A pet"
                    }
                },
            },
        },
    )
    try:
        utils.validate_spec(spec)
    except exceptions.OpenAPIError as error:
        pytest.fail(str(error))
Ejemplo n.º 23
0
def register_swagger(app):
    spec = APISpec(
        title="Renku Notebooks API",
        openapi_version="3.0.2",
        version="v1",
        plugins=[FlaskPlugin(), MarshmallowPlugin()],
        info={
            "description":
            "An API to launch and manage Jupyter servers for Renku. "
            "To get authorized select the `OAuth2, authorizationCode` flow and the "
            "`openid` scope. Scroll up to find the proper flow in the list. "
            "If the deployment supports anonymous sessions you can also use the API "
            "without getting authorized at all."
        },
        security=[{
            "oauth2-swagger": ["openid"]
        }],
        servers=[{
            "url": "/api"
        }],
    )
    # Register schemas
    spec.components.schema("LaunchNotebookRequest",
                           schema=LaunchNotebookRequest)
    spec.components.schema("LaunchNotebookResponse",
                           schema=LaunchNotebookResponse)
    spec.components.schema("ServersGetRequest", schema=ServersGetRequest)
    spec.components.schema("ServersGetResponse", schema=ServersGetResponse)
    spec.components.schema("ServerLogs", schema=ServerLogs)
    spec.components.schema("ServerOptionsUI", schema=ServerOptionsUI)
    spec.components.schema("FailedParsing", schema=FailedParsing)
    spec.components.schema("AutosavesList", schema=AutosavesList)
    spec.components.schema("VersionResponse", schema=VersionResponse)
    # Register endpoints
    with app.test_request_context():
        spec.path(view=user_server)
        spec.path(view=user_servers)
        spec.path(view=launch_notebook)
        spec.path(view=stop_server)
        spec.path(view=server_options)
        spec.path(view=server_logs)
        spec.path(view=autosave_info)
        spec.path(view=delete_autosave)
    # Register security scheme
    security_scheme = {
        "type": "openIdConnect",
        "description": "PKCE flow for swagger.",
        "openIdConnectUrl": config.OIDC_CONFIG_URL,
    }
    spec.components.security_scheme("oauth2-swagger", security_scheme)

    bp = Blueprint("swagger_blueprint",
                   __name__,
                   url_prefix=config.SERVICE_PREFIX)

    @bp.route("spec.json")
    def render_openapi_spec():
        return jsonify(spec.to_dict())

    app.register_blueprint(bp)
    return app
Ejemplo n.º 24
0
    def _generate_spec(self) -> APISpec:
        """Generate the spec, return an instance of `apispec.APISpec`.
        """
        def resolver(schema: Type[Schema]) -> str:
            name = schema.__class__.__name__
            if name.endswith('Schema'):
                name = name[:-6] or name
            if schema.partial:
                name += 'Update'
            return name

        # info object
        info: dict = {}
        if self.contact:
            info['contact'] = self.contact
        if self.license:
            info['license'] = self.license
        if self.terms_of_service:
            info['termsOfService'] = self.terms_of_service
        if self.description:
            info['description'] = self.description
        else:
            # auto-generate info.description from module doc
            if self.config['AUTO_DESCRIPTION']:
                module_name = self.import_name
                while module_name:
                    module = sys.modules[module_name]
                    if module.__doc__:
                        info['description'] = module.__doc__.strip()
                        break
                    if '.' not in module_name:
                        module_name = '.' + module_name
                    module_name = module_name.rsplit('.', 1)[0]

        # tags
        tags: Optional[TagsType] = self.tags
        if tags is not None:
            # Convert simple tags list into standard OpenAPI tags
            if isinstance(tags[0], str):
                for index, tag in enumerate(tags):
                    tags[index] = {'name': tag}  # type: ignore
        else:
            tags: List[str] = []  # type: ignore
            if self.config['AUTO_TAGS']:
                # auto-generate tags from blueprints
                for name, blueprint in self.blueprints.items():
                    if name == 'openapi' or name in self.config[
                            'DOCS_HIDE_BLUEPRINTS']:
                        continue
                    if hasattr(blueprint, 'tag') and blueprint.tag is not None:
                        if isinstance(blueprint.tag, dict):
                            tag = blueprint.tag
                        else:
                            tag = {'name': blueprint.tag}
                    else:
                        tag = {'name': name.title()}
                        module = sys.modules[blueprint.import_name]
                        if module.__doc__:
                            tag['description'] = module.__doc__.strip()
                    tags.append(tag)  # type: ignore

        # additional fields
        kwargs: dict = {}
        if self.servers:
            kwargs['servers'] = self.servers
        if self.external_docs:
            kwargs['externalDocs'] = self.external_docs

        ma_plugin: MarshmallowPlugin = MarshmallowPlugin(
            schema_name_resolver=resolver)
        spec: APISpec = APISpec(title=self.title,
                                version=self.version,
                                openapi_version='3.0.3',
                                plugins=[ma_plugin],
                                info=info,
                                tags=tags,
                                **kwargs)

        # configure flask-marshmallow URL types
        ma_plugin.converter.field_mapping[fields.URLFor] = ('string', 'url')
        ma_plugin.converter.field_mapping[fields.AbsoluteURLFor] = \
            ('string', 'url')
        if sqla is not None:  # pragma: no cover
            ma_plugin.converter.field_mapping[sqla.HyperlinkRelated] = \
                ('string', 'url')

        # security schemes
        auth_schemes: List[HTTPAuthType] = []
        auth_names: List[str] = []
        auth_blueprints: Dict[str, Dict[str, Any]] = {}

        def add_auth_schemes_and_names(auth: HTTPAuthType) -> None:
            auth_schemes.append(auth)
            if isinstance(auth, HTTPBasicAuth):
                name = 'BasicAuth'
            elif isinstance(auth, HTTPTokenAuth):
                if auth.scheme == 'Bearer' and auth.header is None:
                    name = 'BearerAuth'
                else:
                    name = 'ApiKeyAuth'
            else:
                raise RuntimeError('Unknown authentication scheme')
            if name in auth_names:
                v = 2
                new_name = f'{name}_{v}'
                while new_name in auth_names:
                    v += 1
                    new_name = f'{name}_{v}'
                name = new_name
            auth_names.append(name)

        # detect auth_required on before_request functions
        for blueprint_name, funcs in self.before_request_funcs.items():
            for f in funcs:
                if hasattr(f, '_spec'):  # pragma: no cover
                    auth = f._spec.get('auth')  # type: ignore
                    if auth is not None and auth not in auth_schemes:
                        auth_blueprints[blueprint_name] = {  # type: ignore
                            'auth': auth,
                            'roles': f._spec.get('roles')  # type: ignore
                        }
                        add_auth_schemes_and_names(auth)

        for rule in self.url_map.iter_rules():
            view_func = self.view_functions[rule.endpoint]
            if hasattr(view_func, '_spec'):
                auth = view_func._spec.get('auth')
                if auth is not None and auth not in auth_schemes:
                    add_auth_schemes_and_names(auth)

        security: Dict[HTTPAuthType, str] = {}
        security_schemes: Dict[str, Dict[str, str]] = {}
        for name, auth in zip(auth_names, auth_schemes):
            security[auth] = name
            if isinstance(auth, HTTPTokenAuth):
                if auth.scheme == 'Bearer' and auth.header is None:
                    security_schemes[name] = {
                        'type': 'http',
                        'scheme': 'Bearer',
                    }
                else:
                    security_schemes[name] = {
                        'type': 'apiKey',
                        'name': auth.header,
                        'in': 'header',
                    }
            else:
                security_schemes[name] = {
                    'type': 'http',
                    'scheme': 'Basic',
                }

            if hasattr(auth, 'description') and auth.description is not None:
                security_schemes[name]['description'] = auth.description

        for name, scheme in security_schemes.items():
            spec.components.security_scheme(name, scheme)

        # paths
        paths: Dict[str, Dict[str, Any]] = {}
        # rules: List[Any] = list(self.url_map.iter_rules())
        rules: List[Any] = sorted(list(self.url_map.iter_rules()),
                                  key=lambda rule: len(rule.rule))
        for rule in rules:
            operations: Dict[str, Any] = {}
            view_func = self.view_functions[rule.endpoint]
            # skip endpoints from openapi blueprint and the built-in static endpoint
            if rule.endpoint.startswith('openapi') or \
               rule.endpoint.startswith('static'):
                continue
            # skip endpoints from blueprints in config DOCS_HIDE_BLUEPRINTS list
            blueprint_name: Optional[str] = None  # type: ignore
            if '.' in rule.endpoint:
                blueprint_name = rule.endpoint.split('.', 1)[0]
                if blueprint_name in self.config['DOCS_HIDE_BLUEPRINTS']:
                    continue
            # add a default 200 response for bare views
            default_response = {
                'schema': {},
                'status_code': 200,
                'description': None,
                'example': None,
                'examples': None
            }
            if not hasattr(view_func, '_spec'):
                if self.config['AUTO_200_RESPONSE']:
                    view_func._spec = {'response': default_response}
                else:
                    continue  # pragma: no cover
            # skip views flagged with @doc(hide=True)
            if view_func._spec.get('hide'):
                continue

            # tag
            operation_tags: Optional[List[str]] = None
            if view_func._spec.get('tags'):
                operation_tags = view_func._spec.get('tags')
            else:
                # if tag not set, try to use blueprint name as tag
                if self.tags is None and self.config[
                        'AUTO_TAGS'] and blueprint_name is not None:
                    blueprint = self.blueprints[blueprint_name]
                    if hasattr(blueprint, 'tag') and blueprint.tag is not None:
                        if isinstance(blueprint.tag, dict):
                            operation_tags = [blueprint.tag['name']]
                        else:
                            operation_tags = [blueprint.tag]
                    else:
                        operation_tags = [blueprint_name.title()]

            for method in ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']:
                if method not in rule.methods:
                    continue
                operation: Dict[str, Any] = {
                    'parameters': [{
                        'in': location,
                        'schema': schema
                    } for schema, location in view_func._spec.get('args', [])],
                    'responses': {},
                }
                if operation_tags:
                    operation['tags'] = operation_tags

                # summary
                if view_func._spec.get('summary'):
                    operation['summary'] = view_func._spec.get('summary')
                else:
                    # auto-generate summary from dotstring or view function name
                    if self.config['AUTO_PATH_SUMMARY']:
                        docs = (view_func.__doc__ or '').strip().split('\n')
                        if docs[0]:
                            # Use the first line of docstring as summary
                            operation['summary'] = docs[0]
                        else:
                            # Use the function name as summary
                            operation['summary'] = ' '.join(
                                view_func.__name__.split('_')).title()

                # description
                if view_func._spec.get('description'):
                    operation['description'] = view_func._spec.get(
                        'description')
                else:
                    # auto-generate description from dotstring
                    if self.config['AUTO_PATH_DESCRIPTION']:
                        docs = (view_func.__doc__ or '').strip().split('\n')
                        if len(docs) > 1:
                            # Use the remain lines of docstring as description
                            operation['description'] = '\n'.join(
                                docs[1:]).strip()

                # deprecated
                if view_func._spec.get('deprecated'):
                    operation['deprecated'] = view_func._spec.get('deprecated')

                # responses
                def add_response(
                    status_code: str,
                    schema: SchemaType,
                    description: str,
                    example: Optional[Any] = None,
                    examples: Optional[Dict[str, Any]] = None,
                ) -> None:
                    operation['responses'][status_code] = {}
                    if status_code != '204':
                        operation['responses'][status_code]['content'] = {
                            'application/json': {
                                'schema': schema
                            }
                        }
                    operation['responses'][status_code][
                        'description'] = description
                    if example is not None:
                        operation['responses'][status_code]['content'][
                            'application/json']['example'] = example
                    if examples is not None:
                        operation['responses'][status_code]['content'][
                            'application/json']['examples'] = examples

                def add_response_with_schema(status_code: str,
                                             schema: SchemaType,
                                             schema_name: str,
                                             description: str) -> None:
                    if isinstance(schema, type):
                        schema = schema()  # type: ignore
                        add_response(status_code, schema, description)
                    elif isinstance(schema, dict):
                        if schema_name not in spec.components.schemas:
                            spec.components.schema(schema_name, schema)
                        schema_ref = {
                            '$ref': f'#/components/schemas/{schema_name}'
                        }
                        add_response(status_code, schema_ref, description)
                    else:
                        raise RuntimeError(
                            'The schema must be a Marshamallow schema \
                            class or an OpenAPI schema dict.')

                if view_func._spec.get('response'):
                    status_code: str = str(
                        view_func._spec.get('response')['status_code'])
                    schema = view_func._spec.get('response')['schema']
                    description: str = view_func._spec.get('response')['description'] or \
                        self.config['SUCCESS_DESCRIPTION']
                    example = view_func._spec.get('response')['example']
                    examples = view_func._spec.get('response')['examples']
                    add_response(status_code, schema, description, example,
                                 examples)
                else:
                    # add a default 200 response for views without using @output
                    # or @doc(responses={...})
                    if not view_func._spec.get(
                            'responses') and self.config['AUTO_200_RESPONSE']:
                        add_response('200', {},
                                     self.config['SUCCESS_DESCRIPTION'])

                # add validation error response
                if self.config['AUTO_VALIDATION_ERROR_RESPONSE']:
                    if view_func._spec.get('body') or view_func._spec.get(
                            'args'):
                        status_code: str = str(  # type: ignore
                            self.config['VALIDATION_ERROR_STATUS_CODE'])
                        description: str = self.config[  # type: ignore
                            'VALIDATION_ERROR_DESCRIPTION']
                        schema: SchemaType = self.config[
                            'VALIDATION_ERROR_SCHEMA']  # type: ignore
                        add_response_with_schema(status_code, schema,
                                                 'ValidationError',
                                                 description)

                # add authentication error response
                if self.config['AUTO_AUTH_ERROR_RESPONSE']:
                    if view_func._spec.get('auth') or (
                            blueprint_name is not None
                            and blueprint_name in auth_blueprints):
                        status_code: str = str(  # type: ignore
                            self.config['AUTH_ERROR_STATUS_CODE'])
                        description: str = self.config[
                            'AUTH_ERROR_DESCRIPTION']  # type: ignore
                        schema: SchemaType = self.config[
                            'HTTP_ERROR_SCHEMA']  # type: ignore
                        add_response_with_schema(status_code, schema,
                                                 'HTTPError', description)

                if view_func._spec.get('responses'):
                    responses: Union[List[int], Dict[int, str]] \
                        = view_func._spec.get('responses')
                    if isinstance(responses, list):
                        responses: Dict[int, str] = {}  # type: ignore
                        for status_code in view_func._spec.get('responses'):
                            responses[  # type: ignore
                                status_code] = get_reason_phrase(
                                    int(status_code))
                    for status_code, description in responses.items(
                    ):  # type: ignore
                        status_code: str = str(status_code)  # type: ignore
                        if status_code in operation['responses']:
                            continue
                        if status_code.startswith(
                                '4') or status_code.startswith('5'):
                            # add error response schema for error responses
                            schema: SchemaType = self.config[
                                'HTTP_ERROR_SCHEMA']  # type: ignore
                            add_response_with_schema(status_code, schema,
                                                     'HTTPError', description)
                        else:
                            add_response(status_code, {}, description)

                # requestBody
                if view_func._spec.get('body'):
                    operation['requestBody'] = {
                        'content': {
                            'application/json': {
                                'schema': view_func._spec['body'],
                            }
                        }
                    }
                    if view_func._spec.get('body_example'):
                        example = view_func._spec.get('body_example')
                        operation['requestBody']['content'][
                            'application/json']['example'] = example
                    if view_func._spec.get('body_examples'):
                        examples = view_func._spec.get('body_examples')
                        operation['requestBody']['content'][
                            'application/json']['examples'] = examples

                # security
                if blueprint_name is not None and blueprint_name in auth_blueprints:
                    operation['security'] = [{
                        security[auth_blueprints[blueprint_name]['auth']]:
                        auth_blueprints[blueprint_name]['roles']
                    }]

                if view_func._spec.get('auth'):
                    operation['security'] = [{
                        security[view_func._spec['auth']]:
                        view_func._spec['roles']
                    }]

                operations[method.lower()] = operation

            # parameters
            path_arguments: Iterable = re.findall(r'<(([^<:]+:)?([^>]+))>',
                                                  rule.rule)
            if path_arguments:
                arguments: List[Dict[str, str]] = []
                for _, argument_type, argument_name in path_arguments:
                    argument = {
                        'in': 'path',
                        'name': argument_name,
                    }
                    if argument_type == 'int:':
                        argument['schema'] = {'type': 'integer'}
                    elif argument_type == 'float:':
                        argument['schema'] = {'type': 'number'}
                    else:
                        argument['schema'] = {'type': 'string'}
                    arguments.append(argument)

                for method, operation in operations.items():
                    operation[
                        'parameters'] = arguments + operation['parameters']

            path: str = re.sub(r'<([^<:]+:)?', '{',
                               rule.rule).replace('>', '}')
            if path not in paths:
                paths[path] = operations
            else:
                paths[path].update(operations)

        for path, operations in paths.items():
            # sort by method before adding them to the spec
            sorted_operations: Dict[str, Any] = {}
            for method in ['get', 'post', 'put', 'patch', 'delete']:
                if method in operations:
                    sorted_operations[method] = operations[method]
            spec.path(path=path, operations=sorted_operations)

        return spec
Ejemplo n.º 25
0
from copy import deepcopy

import yaml
from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin
from apispec_webframeworks.flask import FlaskPlugin

from flask_base.utils import (
    function_args,
    http_path,
    find_schemas,
    http_methods,
)
from py_tools.format import dumps, loads

mm_plugin = MarshmallowPlugin()
flask_plugin = FlaskPlugin()

api_spec = APISpec(
    title="",
    version="1.0.0",
    openapi_version="2.0",
    plugins=(mm_plugin, flask_plugin),
)


def generate_swagger(cls):
    def update_nested(orig_dict, new_dict):
        """Update nested dictionary"""
        for key, value in new_dict.items():
            if isinstance(value, Mapping):
Ejemplo n.º 26
0
api = falcon.API(middleware=[cors.middleware, cache.middleware])


class spec:
    def on_get(self, req, resp):
        # Create an APISpec
        resp.body = json.dumps(spec.to_dict())
        resp.status = falcon.HTTP_200
        resp.content_type = falcon.MEDIA_JSON


infoResource = info.InfoResource()
api.add_route('/info', infoResource)

circulationSupplyResource = supply.CirculationSupplyResource()
api.add_route('/circulationSupply', circulationSupplyResource)

api.add_route('/doc', spec())

spec = APISpec(
    title='Swagger Haven Network',
    version='1.0.0',
    openapi_version='2.0',
    plugins=[FalconPlugin(api), MarshmallowPlugin()],
)

#spec.components.schema('Supply', schema=supply.SupplySchema)

spec.path(resource=infoResource)
spec.path(resource=circulationSupplyResource)
Ejemplo n.º 27
0
API_DESCRIPTION = """
This is the documentation for the BubBug http service, the platform for Bugzilla Machine Learning projects.

# Introduction

This service can be used to classify a given bug using a pre-trained model.
You can classify a single bug or a batch of bugs.
The classification happens in the background so you need to call back the service for getting the results.
"""

spec = APISpec(
    title="Bugbug",
    version=get_bugbug_version(),
    openapi_version="3.0.2",
    info=dict(description=API_DESCRIPTION),
    plugins=[FlaskPlugin(), MarshmallowPlugin()],
    security=[{
        "api_key": []
    }],
)

application = Flask(__name__)
redis_url = os.environ.get("REDIS_URL", "redis://localhost/0")
redis_conn = Redis.from_url(redis_url)

# Kill jobs which don't finish within 12 minutes.
JOB_TIMEOUT = 12 * 60
# Kill Bugzilla jobs which don't finish within 5 minutes.
BUGZILLA_JOB_TIMEOUT = 5 * 60
# Remove jobs from the queue if they haven't started within 7 minutes.
QUEUE_TIMEOUT = 7 * 60
Ejemplo n.º 28
0
from .apis.workers import blp as workers
from .core.cli import setup_cli
from .core.security import jwt
from .core.util import get_logger
from .settings import MONGO_CONFIG, setup_settings
from .tasks import celery

logger = get_logger(__name__)

app = Flask('NERd')
setup_settings(app)

mongoengine.connect(**MONGO_CONFIG)
jwt.init_app(app)
celery.init_app(app)
api.init_app(app,
             spec_kwargs={'marshmallow_plugin': MarshmallowPlugin(resolver)})
api.register_field(ObjectId, 'string', 'UUID')
api.register_field(Constant, 'string', '')
register_custom_schemas(api.spec)
api.register_blueprint(auth, url_prefix='/api/auth')
api.register_blueprint(users, url_prefix='/api/users')
api.register_blueprint(roles, url_prefix='/api/roles')
api.register_blueprint(corpus, url_prefix='/api/corpus')
api.register_blueprint(snapshots, url_prefix='/api/snapshots')
api.register_blueprint(ner, url_prefix='/api/ner')
api.register_blueprint(trainings, url_prefix='/api/trainings')
api.register_blueprint(workers, url_prefix='/api/workers')

setup_cli(app)
Ejemplo n.º 29
0
        responses:
            200:
                description: A pet to be returned
                schema: PetSchema
        """
        pet = get_random_pet()  # returns JSON
        resp.media = pet


# create instance of resource
random_pet_resource = RandomPetResource()
# pass into `add_route` for Falcon
app.add_route("/random", random_pet_resource)

# Create an APISpec
spec = APISpec(
    title='Swagger Petstore',
    version='1.0.0',
    openapi_version='2.0',
    plugins=[
        FalconPlugin(app),
        MarshmallowPlugin(),
    ],
)

# Register entities and paths
spec.components.schema('Category', schema=CategorySchema)
spec.components.schema('Pet', schema=PetSchema)
# pass created resource into `path` for APISpec
spec.path(resource=random_pet_resource)
Ejemplo n.º 30
0
def register_swagger(app,
                     title,
                     version,
                     openapi_version="3.0.2",
                     swagger_url='/api/docs',
                     security_schemes=None,
                     username=None,
                     password=None):
    """
    Generates specs and registers SwaggerUI blueprint
    :param app:
    :param str title: app title
    :param str version: app version
    :param str openapi_version:
    :param str swagger_url: URL for swaggerUI
    :param dict security_schemes: Swagger security schemas
        To get Examples see:
        https://apispec.readthedocs.io/en/latest/special_topics.html#documenting-security-schemes
        https://swagger.io/docs/specification/authentication/
    :param str username: Username for basic auth.
        No auth on swagger blueprint if None
    :param str password: Password for basic auth
    :return:
    """
    # Generate specs
    app.spec = APISpec(
        title=title,
        version=version,
        openapi_version=openapi_version,
        plugins=[FlaskPlugin(), MarshmallowPlugin()],
    )
    with app.test_request_context():
        logger.debug('Registering view functions in APISpec...')
        for key, view in app.view_functions.items():
            logger.debug('Register view:%s', key)
            app.spec.path(view=view,
                          parameters=get_inline_params(app, endpoint=key))

    # Add security schemes if needed
    if security_schemes:
        assert isinstance(security_schemes, dict)
        for lbl, info in security_schemes.items():
            app.spec.components.security_scheme(lbl, info)

    # Add Swagger UI
    api_path = '/swagger.json'
    blueprint = get_swaggerui_blueprint(
        base_url=
        swagger_url,  # Swagger UI static files will be mapped to '{SWAGGER_URL}/dist/'
        api_url=swagger_url + api_path,
        config=None,  # Swagger UI config overrides
        # oauth_config={  # OAuth config. See https://github.com/swagger-api/swagger-ui#oauth2-configuration.
        #    'clientId': "your-client-id",
        #    'clientSecret': "your-client-secret-if-required",
        #    'realm': "your-realms",
        #    'appName': "your-app-name",
        #    'scopeSeparator': " ",
        #    'additionalQueryStringParams': {'test': "hello"}
        # }
    )

    # Add Swagger JSON route
    @blueprint.route(api_path)
    @basic_auth_decorator(
        username=username,
        password=password,
    )
    def swagger_json_view():
        return jsonify(app.spec.to_dict())

    # Register swaggerUI blueprint
    app.register_blueprint(blueprint, url_prefix=swagger_url)