# :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.
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
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')
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
# 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
: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))
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)
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
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
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))
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)
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)
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))
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
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))
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
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))
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
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)
def marshmallow_plugin(): return MarshmallowPlugin()
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))
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
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
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):
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)
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
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)
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)
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)