Skip to content

Commit

Permalink
Allow multiple flask instances per Api instance
Browse files Browse the repository at this point in the history
  • Loading branch information
lafrech committed Nov 28, 2018
1 parent 86b2460 commit b7d0985
Show file tree
Hide file tree
Showing 10 changed files with 125 additions and 138 deletions.
52 changes: 22 additions & 30 deletions flask_rest_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,13 @@ class Api(DocBlueprintMixin, ErrorHandlerMixin):
according to the app configuration.
"""
def __init__(self, app=None, *, spec_kwargs=None):
self._app = app
self.spec = None
self._specs = {}
if app is not None:
self.init_app(app, spec_kwargs=spec_kwargs)

def init_app(self, app, *, spec_kwargs=None):
"""Initialize Api with application"""

self._app = app

# Register flask-rest-api in app extensions
app.extensions = getattr(app, 'extensions', {})
ext = app.extensions.setdefault('flask-rest-api', {})
Expand All @@ -54,19 +51,20 @@ def init_app(self, app, *, spec_kwargs=None):
if base_path != '/':
spec_kwargs.setdefault('basePath', base_path)
spec_kwargs.update(app.config.get('API_SPEC_OPTIONS', {}))
self.spec = APISpec(
# Keep one spec per app instance
self._specs[app] = APISpec(
app.name,
app.config.get('API_VERSION', '1'),
openapi_version=openapi_version,
**spec_kwargs,
)
# Initialize blueprint serving spec
self._register_doc_blueprint()
self._register_doc_blueprint(app)

# Register error handlers
self._register_error_handlers()
self._register_error_handlers(app)

def register_blueprint(self, blp, **options):
def register_blueprint(self, app, blp, **options):
"""Register a blueprint in the application
Also registers documentation for the blueprint/resource
Expand All @@ -75,17 +73,17 @@ def register_blueprint(self, blp, **options):
:param dict options: Keyword arguments overriding Blueprint defaults
"""

self._app.register_blueprint(blp, **options)
app.register_blueprint(blp, **options)

# Register views in API documentation for this resource
blp.register_views_in_doc(self._app, self.spec)
blp.register_views_in_doc(app, self._specs[app])

# Add tag relative to this resource to the global tag list
tag = {'name': blp.name, 'description': blp.description}
if APISPEC_VERSION_MAJOR < 1:
self.spec.add_tag(tag)
self._specs[app].add_tag(tag)
else:
self.spec.tag(tag)
self._specs[app].tag(tag)

def definition(self, name):
"""Decorator to register a Schema in the doc
Expand All @@ -102,28 +100,26 @@ class PetSchema(Schema):
...
"""
def decorator(schema_cls, **kwargs):
if APISPEC_VERSION_MAJOR < 1:
self.spec.definition(name, schema=schema_cls, **kwargs)
else:
self.spec.components.schema(name, schema=schema_cls, **kwargs)
for spec in self._specs.values():
if APISPEC_VERSION_MAJOR < 1:
spec.definition(name, schema=schema_cls, **kwargs)
else:
spec.components.schema(name, schema=schema_cls, **kwargs)
return schema_cls
return decorator

def register_converter(self, converter, conv_type, conv_format=None,
*, name=None):
def register_converter(self, converter, conv_type, conv_format=None):
"""Register custom path parameter converter
:param BaseConverter converter: Converter
Subclass of werkzeug's BaseConverter
:param str conv_type: Parameter type
:param str conv_format: Parameter format (optional)
:param str name: Name of the converter. If not None, this name is used
to register the converter in the Flask app.
Example: ::
api.register_converter(
UUIDConverter, 'string', 'UUID', name='uuid')
app.url_map.converters['uuid'] = UUIDConverter
api.register_converter(UUIDConverter, 'string', 'UUID')
@blp.route('/pets/{uuid:pet_id}')
...
Expand All @@ -135,14 +131,9 @@ def register_converter(self, converter, conv_type, conv_format=None,
Once the converter is registered, all paths using it will have
corresponding path parameter documented with the right type and format.
The `name` parameter need not be passed if the converter is already
registered in the app, for instance if it belongs to a Flask extension
that already registers it in the app.
"""
if name:
self._app.url_map.converters[name] = converter
self.spec.register_converter(converter, conv_type, conv_format)
for spec in self._specs.values():
spec.register_converter(converter, conv_type, conv_format)

def register_field(self, field, *args):
"""Register custom Marshmallow field
Expand All @@ -168,4 +159,5 @@ def register_field(self, field, *args):
# Map to ('integer, 'int32')
api.register_field(CustomIntegerField, ma.fields.Integer)
"""
self.spec.register_field(field, *args)
for spec in self._specs.values():
spec.register_field(field, *args)
5 changes: 2 additions & 3 deletions flask_rest_api/error_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@
class ErrorHandlerMixin:
"""Extend Api to manage error handling."""

def _register_error_handlers(self):
def _register_error_handlers(self, app):
"""Register error handlers in Flask app
This method registers a default error handler for HTTPException.
"""
self._app.register_error_handler(
HTTPException, self.handle_http_exception)
app.register_error_handler(HTTPException, self.handle_http_exception)

def handle_http_exception(self, error):
"""Return error description and details in response body
Expand Down
104 changes: 54 additions & 50 deletions flask_rest_api/spec/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import json

import flask
from flask import current_app
import apispec

from .plugins import FlaskPlugin
Expand Down Expand Up @@ -72,33 +71,45 @@ def _add_leading_slash(string):
class DocBlueprintMixin:
"""Extend Api to serve the spec in a dedicated blueprint."""

def _register_doc_blueprint(self):
def _register_doc_blueprint(self, app):
"""Register a blueprint in the application to expose the spec
Doc Blueprint contains routes to
- json spec file
- spec UI (ReDoc, Swagger UI).
"""
api_url = self._app.config.get('OPENAPI_URL_PREFIX', None)
api_url = app.config.get('OPENAPI_URL_PREFIX', None)
if api_url is not None:
blueprint = flask.Blueprint(
'api-docs',
__name__,
url_prefix=_add_leading_slash(api_url),
template_folder='./templates',
)
# Serve json spec at 'url_prefix/openapi.json' by default
json_path = self._app.config.get(
'OPENAPI_JSON_PATH', 'openapi.json')
blueprint.add_url_rule(
_add_leading_slash(json_path),
endpoint='openapi_json',
view_func=self._openapi_json)
self._register_redoc_rule(blueprint)
self._register_swagger_ui_rule(blueprint)
self._app.register_blueprint(blueprint)

def _register_redoc_rule(self, blueprint):
self._register_openapi_json_rule(app, blueprint)
self._register_redoc_rule(app, blueprint)
self._register_swagger_ui_rule(app, blueprint)
app.register_blueprint(blueprint)

def _register_openapi_json_rule(self, app, blueprint):
"""Serve json spec file"""
json_path = app.config.get('OPENAPI_JSON_PATH', 'openapi.json')

def openapi_json():
"""Serve JSON spec file"""
# We don't use Flask.jsonify here as it would sort the keys
# alphabetically while we want to preserve the order.
return app.response_class(
json.dumps(self._specs[app].to_dict(), indent=2),
mimetype='application/json')

blueprint.add_url_rule(
_add_leading_slash(json_path),
endpoint='openapi_json',
view_func=openapi_json)

@staticmethod
def _register_redoc_rule(app, blueprint):
"""Register ReDoc rule
The ReDoc script URL can be specified as OPENAPI_REDOC_URL.
Expand All @@ -115,12 +126,12 @@ def _register_redoc_rule(self, blueprint):
OPENAPI_REDOC_VERSION is ignored when OPENAPI_REDOC_URL is passed.
"""
redoc_path = self._app.config.get('OPENAPI_REDOC_PATH')
redoc_path = app.config.get('OPENAPI_REDOC_PATH')
if redoc_path is not None:
redoc_url = self._app.config.get('OPENAPI_REDOC_URL')
redoc_url = app.config.get('OPENAPI_REDOC_URL')
if redoc_url is None:
# TODO: default to 'next' when ReDoc 2.0.0 is released.
redoc_version = self._app.config.get(
redoc_version = app.config.get(
'OPENAPI_REDOC_VERSION', 'latest')
# latest or v1.x -> Redoc GitHub CDN
if redoc_version == 'latest' or redoc_version.startswith('v1'):
Expand All @@ -132,13 +143,19 @@ def _register_redoc_rule(self, blueprint):
redoc_url = (
'https://cdn.jsdelivr.net/npm/redoc@'
'{}/bundles/redoc.standalone.js'.format(redoc_version))
self._redoc_url = redoc_url

def openapi_redoc():
"""Expose OpenAPI spec with ReDoc"""
return flask.render_template(
'redoc.html', title=app.name, redoc_url=redoc_url)

blueprint.add_url_rule(
_add_leading_slash(redoc_path),
endpoint='openapi_redoc',
view_func=self._openapi_redoc)
view_func=openapi_redoc)

def _register_swagger_ui_rule(self, blueprint):
@staticmethod
def _register_swagger_ui_rule(app, blueprint):
"""Register Swagger UI rule
The Swagger UI scripts base URL can be specified as
Expand All @@ -154,47 +171,34 @@ def _register_swagger_ui_rule(self, blueprint):
OPENAPI_SWAGGER_UI_SUPPORTED_SUBMIT_METHODS specifes the methods for
which the 'Try it out!' feature is enabled.
"""
swagger_ui_path = self._app.config.get('OPENAPI_SWAGGER_UI_PATH')
swagger_ui_path = app.config.get('OPENAPI_SWAGGER_UI_PATH')
if swagger_ui_path is not None:
swagger_ui_url = self._app.config.get('OPENAPI_SWAGGER_UI_URL')
swagger_ui_url = app.config.get('OPENAPI_SWAGGER_UI_URL')
if swagger_ui_url is None:
swagger_ui_version = self._app.config.get(
swagger_ui_version = app.config.get(
'OPENAPI_SWAGGER_UI_VERSION')
if swagger_ui_version is not None:
swagger_ui_url = (
'https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/'
'{}/'.format(swagger_ui_version))
if swagger_ui_url is not None:
self._swagger_ui_url = swagger_ui_url
self._swagger_ui_supported_submit_methods = (
self._app.config.get(
swagger_ui_supported_submit_methods = (
app.config.get(
'OPENAPI_SWAGGER_UI_SUPPORTED_SUBMIT_METHODS',
['get', 'put', 'post', 'delete', 'options',
'head', 'patch', 'trace'])
)

def openapi_swagger_ui():
"""Expose OpenAPI spec with Swagger UI"""
return flask.render_template(
'swagger_ui.html', title=app.name,
swagger_ui_url=swagger_ui_url,
swagger_ui_supported_submit_methods=(
swagger_ui_supported_submit_methods)
)

blueprint.add_url_rule(
_add_leading_slash(swagger_ui_path),
endpoint='openapi_swagger_ui',
view_func=self._openapi_swagger_ui)

def _openapi_json(self):
"""Serve JSON spec file"""
# We don't use Flask.jsonify here as it would sort the keys
# alphabetically while we want to preserve the order.
return current_app.response_class(
json.dumps(self.spec.to_dict(), indent=2),
mimetype='application/json')

def _openapi_redoc(self):
"""Expose OpenAPI spec with ReDoc"""
return flask.render_template(
'redoc.html', title=self._app.name, redoc_url=self._redoc_url)

def _openapi_swagger_ui(self):
"""Expose OpenAPI spec with Swagger UI"""
return flask.render_template(
'swagger_ui.html', title=self._app.name,
swagger_ui_url=self._swagger_ui_url,
swagger_ui_supported_submit_methods=(
self._swagger_ui_supported_submit_methods)
)
view_func=openapi_swagger_ui)
Loading

0 comments on commit b7d0985

Please sign in to comment.