New style of passing options to Connexion (#436)

* Order classes by relevance in module

* Order definitions by relevance within module

* Swagger UI options extracted

* New style options

* Use new-style options

* Reuse code

* Sort imports

* Ignore typing imports

* Warn users about parameter name change

* Add back isort check

* Fix isort check
This commit is contained in:
Rafael Carício
2017-04-11 16:47:21 +02:00
committed by Henning Jacobs
parent 19e0b37194
commit 93c06711ed
12 changed files with 400 additions and 278 deletions

View File

@@ -3,6 +3,7 @@ import copy
import logging import logging
import pathlib import pathlib
import sys import sys
from typing import AnyStr, List # NOQA
import jinja2 import jinja2
import six import six
@@ -11,6 +12,7 @@ from swagger_spec_validator.validator20 import validate_spec
from ..exceptions import ResolverError from ..exceptions import ResolverError
from ..operation import Operation from ..operation import Operation
from ..options import ConnexionOptions
from ..resolver import Resolver from ..resolver import Resolver
MODULE_PATH = pathlib.Path(__file__).absolute().parent.parent MODULE_PATH = pathlib.Path(__file__).absolute().parent.parent
@@ -19,35 +21,7 @@ SWAGGER_UI_URL = 'ui'
RESOLVER_ERROR_ENDPOINT_RANDOM_DIGITS = 6 RESOLVER_ERROR_ENDPOINT_RANDOM_DIGITS = 6
logger = logging.getLogger('connexion.apis') logger = logging.getLogger('connexion.apis.abstract')
def canonical_base_url(base_path):
"""
Make given "basePath" a canonical base URL which can be prepended to paths starting with "/".
"""
return base_path.rstrip('/')
def compatibility_layer(spec):
"""Make specs compatible with older versions of Connexion."""
if not isinstance(spec, dict):
return spec
# Make all response codes be string
for path_name, methods_available in spec.get('paths', {}).items():
for method_name, method_def in methods_available.items():
if (method_name == 'parameters' or not isinstance(
method_def, dict)):
continue
response_definitions = {}
for response_code, response_def in method_def.get(
'responses', {}).items():
response_definitions[str(response_code)] = response_def
method_def['responses'] = response_definitions
return spec
@six.add_metaclass(abc.ABCMeta) @six.add_metaclass(abc.ABCMeta)
@@ -56,19 +30,14 @@ class AbstractAPI(object):
Defines an abstract interface for a Swagger API Defines an abstract interface for a Swagger API
""" """
def __init__(self, specification, jsonifier, base_url=None, arguments=None, def __init__(self, specification, base_path=None, arguments=None,
swagger_json=None, swagger_ui=None, swagger_path=None, swagger_url=None,
validate_responses=False, strict_validation=False, resolver=None, validate_responses=False, strict_validation=False, resolver=None,
auth_all_paths=False, debug=False, resolver_error_handler=None, auth_all_paths=False, debug=False, resolver_error_handler=None,
validator_map=None, pythonic_params=False): validator_map=None, pythonic_params=False, options=None, **old_style_options):
""" """
:type specification: pathlib.Path | dict :type specification: pathlib.Path | dict
:type base_url: str | None :type base_path: str | None
:type arguments: dict | None :type arguments: dict | None
:type swagger_json: bool
:type swagger_ui: bool
:type swagger_path: string | None
:type swagger_url: string | None
:type validate_responses: bool :type validate_responses: bool
:type strict_validation: bool :type strict_validation: bool
:type auth_all_paths: bool :type auth_all_paths: bool
@@ -82,17 +51,31 @@ class AbstractAPI(object):
:param pythonic_params: When True CamelCase parameters are converted to snake_case and an underscore is appended :param pythonic_params: When True CamelCase parameters are converted to snake_case and an underscore is appended
to any shadowed built-ins to any shadowed built-ins
:type pythonic_params: bool :type pythonic_params: bool
:param options: New style options dictionary.
:type options: dict | None
:param old_style_options: Old style options support for backward compatibility. Preference is
what is defined in `options` parameter.
""" """
self.debug = debug self.debug = debug
self.validator_map = validator_map self.validator_map = validator_map
self.resolver_error_handler = resolver_error_handler self.resolver_error_handler = resolver_error_handler
self.options = ConnexionOptions(old_style_options)
# options is added last to preserve the highest priority
self.options = self.options.extend(options)
# TODO: Remove this in later versions (Current version is 1.1.9)
if base_path is None and 'base_url' in old_style_options:
base_path = old_style_options['base_url']
logger.warning("Parameter base_url should be no longer used. Use base_path instead.")
logger.debug('Loading specification: %s', specification, logger.debug('Loading specification: %s', specification,
extra={'swagger_yaml': specification, extra={'swagger_yaml': specification,
'base_url': base_url, 'base_path': base_path,
'arguments': arguments, 'arguments': arguments,
'swagger_ui': swagger_ui, 'swagger_ui': self.options.openapi_console_ui_available,
'swagger_path': swagger_path, 'swagger_path': self.options.openapi_console_ui_from_dir,
'swagger_url': swagger_url, 'swagger_url': self.options.openapi_console_ui_path,
'auth_all_paths': auth_all_paths}) 'auth_all_paths': auth_all_paths})
if isinstance(specification, dict): if isinstance(specification, dict):
@@ -108,12 +91,9 @@ class AbstractAPI(object):
spec = copy.deepcopy(self.specification) spec = copy.deepcopy(self.specification)
validate_spec(spec) validate_spec(spec)
self.swagger_path = swagger_path or SWAGGER_UI_PATH
self.swagger_url = swagger_url or SWAGGER_UI_URL
# https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#fixed-fields # https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#fixed-fields
# If base_url is not on provided then we try to read it from the swagger.yaml or use / by default # If base_path is not on provided then we try to read it from the swagger.yaml or use / by default
self._set_base_url(base_url) self._set_base_path(base_path)
# A list of MIME types the APIs can produce. This is global to all APIs but can be overridden on specific # A list of MIME types the APIs can produce. This is global to all APIs but can be overridden on specific
# API calls. # API calls.
@@ -142,11 +122,10 @@ class AbstractAPI(object):
logger.debug('Pythonic params: %s', str(pythonic_params)) logger.debug('Pythonic params: %s', str(pythonic_params))
self.pythonic_params = pythonic_params self.pythonic_params = pythonic_params
self.jsonifier = jsonifier if self.options.openapi_spec_available:
if swagger_json:
self.add_swagger_json() self.add_swagger_json()
if swagger_ui:
if self.options.openapi_console_ui_available:
self.add_swagger_ui() self.add_swagger_ui()
self.add_paths() self.add_paths()
@@ -154,23 +133,24 @@ class AbstractAPI(object):
if auth_all_paths: if auth_all_paths:
self.add_auth_on_not_found(self.security, self.security_definitions) self.add_auth_on_not_found(self.security, self.security_definitions)
def _set_base_url(self, base_url): def _set_base_path(self, base_path):
if base_url is None: # type: (AnyStr) -> None
self.base_url = canonical_base_url(self.specification.get('basePath', '')) if base_path is None:
self.base_path = canonical_base_path(self.specification.get('basePath', ''))
else: else:
self.base_url = canonical_base_url(base_url) self.base_path = canonical_base_path(base_path)
self.specification['basePath'] = base_url self.specification['basePath'] = base_path
@abc.abstractmethod @abc.abstractmethod
def add_swagger_json(self): def add_swagger_json(self):
""" """
Adds swagger json to {base_url}/swagger.json Adds swagger json to {base_path}/swagger.json
""" """
@abc.abstractmethod @abc.abstractmethod
def add_swagger_ui(self): def add_swagger_ui(self):
""" """
Adds swagger ui to {base_url}/ui/ Adds swagger ui to {base_path}/ui/
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -249,7 +229,7 @@ class AbstractAPI(object):
""" """
paths = paths or self.specification.get('paths', dict()) paths = paths or self.specification.get('paths', dict())
for path, methods in paths.items(): for path, methods in paths.items():
logger.debug('Adding %s%s...', self.base_url, path) logger.debug('Adding %s%s...', self.base_path, path)
# search for parameters definitions in the path level # search for parameters definitions in the path level
# http://swagger.io/specification/#pathItemObject # http://swagger.io/specification/#pathItemObject
@@ -275,7 +255,7 @@ class AbstractAPI(object):
self._handle_add_operation_error(path, method, sys.exc_info()) self._handle_add_operation_error(path, method, sys.exc_info())
def _handle_add_operation_error(self, path, method, exc_info): def _handle_add_operation_error(self, path, method, exc_info):
url = '{base_url}{path}'.format(base_url=self.base_url, path=path) url = '{base_path}{path}'.format(base_path=self.base_path, path=path)
error_msg = 'Failed to add operation for {method} {url}'.format( error_msg = 'Failed to add operation for {method} {url}'.format(
method=method.upper(), method=method.upper(),
url=url) url=url)
@@ -317,3 +297,41 @@ class AbstractAPI(object):
:type response: ConnexionResponse :type response: ConnexionResponse
:type mimetype: str :type mimetype: str
""" """
@classmethod
@abc.abstractmethod
def json_loads(self, data):
"""
API specific JSON loader.
:param data:
:return:
"""
def canonical_base_path(base_path):
"""
Make given "basePath" a canonical base URL which can be prepended to paths starting with "/".
"""
return base_path.rstrip('/')
def compatibility_layer(spec):
"""Make specs compatible with older versions of Connexion."""
if not isinstance(spec, dict):
return spec
# Make all response codes be string
for path_name, methods_available in spec.get('paths', {}).items():
for method_name, method_def in methods_available.items():
if (method_name == 'parameters' or not isinstance(
method_def, dict)):
continue
response_definitions = {}
for response_code, response_def in method_def.get(
'responses', {}).items():
response_definitions[str(response_code)] = response_def
method_def['responses'] = response_definitions
return spec

View File

@@ -6,7 +6,7 @@ import werkzeug.exceptions
from connexion.apis import flask_utils from connexion.apis import flask_utils
from connexion.apis.abstract import AbstractAPI from connexion.apis.abstract import AbstractAPI
from connexion.decorators.produces import BaseSerializer, NoContent from connexion.decorators.produces import NoContent
from connexion.handlers import AuthErrorHandler from connexion.handlers import AuthErrorHandler
from connexion.lifecycle import ConnexionRequest, ConnexionResponse from connexion.lifecycle import ConnexionRequest, ConnexionResponse
from connexion.utils import is_json_mimetype from connexion.utils import is_json_mimetype
@@ -14,68 +14,28 @@ from connexion.utils import is_json_mimetype
logger = logging.getLogger('connexion.apis.flask_api') logger = logging.getLogger('connexion.apis.flask_api')
class Jsonifier(BaseSerializer):
@staticmethod
def dumps(data):
""" Central point where JSON serialization happens inside
Connexion.
"""
return "{}\n".format(flask.json.dumps(data, indent=2))
@staticmethod
def loads(data):
""" Central point where JSON serialization happens inside
Connexion.
"""
if isinstance(data, six.binary_type):
data = data.decode()
try:
return flask.json.loads(data)
except Exception as error:
if isinstance(data, six.string_types):
return data
def __repr__(self):
"""
:rtype: str
"""
return '<Jsonifier: {}>'.format(self.mimetype)
class FlaskApi(AbstractAPI): class FlaskApi(AbstractAPI):
jsonifier = Jsonifier def _set_base_path(self, base_path):
super(FlaskApi, self)._set_base_path(base_path)
def __init__(self, specification, base_url=None, arguments=None,
swagger_json=None, swagger_ui=None, swagger_path=None, swagger_url=None,
validate_responses=False, strict_validation=False, resolver=None,
auth_all_paths=False, debug=False, resolver_error_handler=None,
validator_map=None, pythonic_params=False):
super(FlaskApi, self).__init__(
specification, FlaskApi.jsonifier, base_url=base_url, arguments=arguments,
swagger_json=swagger_json, swagger_ui=swagger_ui,
swagger_path=swagger_path, swagger_url=swagger_url,
validate_responses=validate_responses, strict_validation=strict_validation,
resolver=resolver, auth_all_paths=auth_all_paths, debug=debug,
resolver_error_handler=resolver_error_handler, validator_map=validator_map,
pythonic_params=pythonic_params
)
def _set_base_url(self, base_url):
super(FlaskApi, self)._set_base_url(base_url)
self._set_blueprint() self._set_blueprint()
def _set_blueprint(self): def _set_blueprint(self):
logger.debug('Creating API blueprint: %s', self.base_url) logger.debug('Creating API blueprint: %s', self.base_path)
endpoint = flask_utils.flaskify_endpoint(self.base_url) endpoint = flask_utils.flaskify_endpoint(self.base_path)
self.blueprint = flask.Blueprint(endpoint, __name__, url_prefix=self.base_url, self.blueprint = flask.Blueprint(endpoint, __name__, url_prefix=self.base_path,
template_folder=str(self.swagger_path)) template_folder=str(self.options.openapi_console_ui_from_dir))
def json_loads(self, data):
"""
Use Flask specific JSON loader
"""
return Jsonifier.loads(data)
def add_swagger_json(self): def add_swagger_json(self):
""" """
Adds swagger json to {base_url}/swagger.json Adds swagger json to {base_path}/swagger.json
""" """
logger.debug('Adding swagger.json: %s/swagger.json', self.base_url) logger.debug('Adding swagger.json: %s/swagger.json', self.base_path)
endpoint_name = "{name}_swagger_json".format(name=self.blueprint.name) endpoint_name = "{name}_swagger_json".format(name=self.blueprint.name)
self.blueprint.add_url_rule('/swagger.json', self.blueprint.add_url_rule('/swagger.json',
endpoint_name, endpoint_name,
@@ -83,24 +43,28 @@ class FlaskApi(AbstractAPI):
def add_swagger_ui(self): def add_swagger_ui(self):
""" """
Adds swagger ui to {base_url}/ui/ Adds swagger ui to {base_path}/ui/
""" """
logger.debug('Adding swagger-ui: %s/%s/', self.base_url, self.swagger_url) console_ui_path = self.options.openapi_console_ui_path.strip('/')
logger.debug('Adding swagger-ui: %s/%s/',
self.base_path,
console_ui_path)
static_endpoint_name = "{name}_swagger_ui_static".format(name=self.blueprint.name) static_endpoint_name = "{name}_swagger_ui_static".format(name=self.blueprint.name)
self.blueprint.add_url_rule('/{swagger_url}/<path:filename>'.format(swagger_url=self.swagger_url), static_files_url = '/{console_ui_path}/<path:filename>'.format(
static_endpoint_name, self.swagger_ui_static) console_ui_path=console_ui_path)
self.blueprint.add_url_rule(static_files_url,
static_endpoint_name,
self._handlers.console_ui_static_files)
index_endpoint_name = "{name}_swagger_ui_index".format(name=self.blueprint.name) index_endpoint_name = "{name}_swagger_ui_index".format(name=self.blueprint.name)
self.blueprint.add_url_rule('/{swagger_url}/'.format(swagger_url=self.swagger_url), console_ui_url = '/{swagger_url}/'.format(
index_endpoint_name, self.swagger_ui_index) swagger_url=self.options.openapi_console_ui_path.strip('/'))
def swagger_ui_index(self): self.blueprint.add_url_rule(console_ui_url,
return flask.render_template('index.html', api_url=self.base_url) index_endpoint_name,
self._handlers.console_ui_home)
def swagger_ui_static(self, filename):
"""
:type filename: str
"""
return flask.send_from_directory(str(self.swagger_path), filename)
def add_auth_on_not_found(self, security, security_definitions): def add_auth_on_not_found(self, security, security_definitions):
""" """
@@ -123,6 +87,13 @@ class FlaskApi(AbstractAPI):
function = operation.function function = operation.function
self.blueprint.add_url_rule(flask_path, endpoint_name, function, methods=[method]) self.blueprint.add_url_rule(flask_path, endpoint_name, function, methods=[method])
@property
def _handlers(self):
# type: () -> InternalHandlers
if not hasattr(self, '_internal_handlers'):
self._internal_handlers = InternalHandlers(self.base_path, self.options)
return self._internal_handlers
@classmethod @classmethod
def get_response(cls, response, mimetype=None, request=None): def get_response(cls, response, mimetype=None, request=None):
"""Gets ConnexionResponse instance for the operation handler """Gets ConnexionResponse instance for the operation handler
@@ -198,7 +169,7 @@ class FlaskApi(AbstractAPI):
def _jsonify_data(cls, data, mimetype): def _jsonify_data(cls, data, mimetype):
if (isinstance(mimetype, six.string_types) and is_json_mimetype(mimetype)) \ if (isinstance(mimetype, six.string_types) and is_json_mimetype(mimetype)) \
or not (isinstance(data, six.binary_type) or isinstance(data, six.text_type)): or not (isinstance(data, six.binary_type) or isinstance(data, six.text_type)):
return cls.jsonifier.dumps(data) return Jsonifier.dumps(data)
return data return data
@@ -262,6 +233,7 @@ class FlaskRequestContextProxy(object):
""""Proxy assignments from `ConnexionRequest.context` """"Proxy assignments from `ConnexionRequest.context`
to `flask.request` instance. to `flask.request` instance.
""" """
def __init__(self): def __init__(self):
self.values = {} self.values = {}
@@ -274,3 +246,55 @@ class FlaskRequestContextProxy(object):
def items(self): def items(self):
# type: () -> list # type: () -> list
return self.values.items() return self.values.items()
class Jsonifier(object):
@staticmethod
def dumps(data):
""" Central point where JSON serialization happens inside
Connexion.
"""
return "{}\n".format(flask.json.dumps(data, indent=2))
@staticmethod
def loads(data):
""" Central point where JSON serialization happens inside
Connexion.
"""
if isinstance(data, six.binary_type):
data = data.decode()
try:
return flask.json.loads(data)
except Exception as error:
if isinstance(data, six.string_types):
return data
class InternalHandlers(object):
"""
Flask handlers for internally registered endpoints.
"""
def __init__(self, base_path, options):
self.base_path = base_path
self.options = options
def console_ui_home(self):
"""
Home page of the OpenAPI Console UI.
:return:
"""
return flask.render_template('index.html', api_url=self.base_path)
def console_ui_static_files(self, filename):
"""
Servers the static files for the OpenAPI Console UI.
:param filename: Requested file contents.
:return:
"""
# convert PosixPath to str
static_dir = str(self.options.openapi_console_ui_from_dir)
return flask.send_from_directory(static_dir, filename)

View File

@@ -4,6 +4,7 @@ import pathlib
import six import six
from ..options import ConnexionOptions
from ..resolver import Resolver from ..resolver import Resolver
logger = logging.getLogger('connexion.app') logger = logging.getLogger('connexion.app')
@@ -12,9 +13,8 @@ logger = logging.getLogger('connexion.app')
@six.add_metaclass(abc.ABCMeta) @six.add_metaclass(abc.ABCMeta)
class AbstractApp(object): class AbstractApp(object):
def __init__(self, import_name, api_cls, port=None, specification_dir='', def __init__(self, import_name, api_cls, port=None, specification_dir='',
server=None, arguments=None, auth_all_paths=False, host=None, server=None, arguments=None, auth_all_paths=False, debug=False,
debug=False, swagger_json=True, swagger_ui=True, swagger_path=None, validator_map=None, options=None, **old_style_options):
swagger_url=None, host=None, validator_map=None):
""" """
:param import_name: the name of the application package :param import_name: the name of the application package
:type import_name: str :type import_name: str
@@ -32,14 +32,6 @@ class AbstractApp(object):
:type auth_all_paths: bool :type auth_all_paths: bool
:param debug: include debugging information :param debug: include debugging information
:type debug: bool :type debug: bool
:param swagger_json: whether to include swagger json or not
:type swagger_json: bool
:param swagger_ui: whether to include swagger ui or not
:type swagger_ui: bool
:param swagger_path: path to swagger-ui directory
:type swagger_path: string | None
:param swagger_url: URL to access swagger-ui documentation
:type swagger_url: string | None
:param validator_map: map of validators :param validator_map: map of validators
:type validator_map: dict :type validator_map: dict
""" """
@@ -48,14 +40,16 @@ class AbstractApp(object):
self.debug = debug self.debug = debug
self.import_name = import_name self.import_name = import_name
self.arguments = arguments or {} self.arguments = arguments or {}
self.swagger_json = swagger_json
self.swagger_ui = swagger_ui
self.swagger_path = swagger_path
self.swagger_url = swagger_url
self.auth_all_paths = auth_all_paths
self.resolver_error = None
self.validator_map = validator_map
self.api_cls = api_cls self.api_cls = api_cls
self.resolver_error = None
# Options
self.auth_all_paths = auth_all_paths
self.validator_map = validator_map
self.options = ConnexionOptions(old_style_options)
# options is added last to preserve the highest priority
self.options = self.options.extend(options) # type: ConnexionOptions
self.app = self.create_app() self.app = self.create_app()
self.server = server self.server = server
@@ -94,10 +88,9 @@ class AbstractApp(object):
""" """
def add_api(self, specification, base_path=None, arguments=None, def add_api(self, specification, base_path=None, arguments=None,
auth_all_paths=None, swagger_json=None, swagger_ui=None, auth_all_paths=None, validate_responses=False,
swagger_path=None, swagger_url=None, validate_responses=False,
strict_validation=False, resolver=Resolver(), resolver_error=None, strict_validation=False, resolver=Resolver(), resolver_error=None,
pythonic_params=False): pythonic_params=False, options=None, **old_style_options):
""" """
Adds an API to the application based on a swagger file or API dict Adds an API to the application based on a swagger file or API dict
@@ -109,14 +102,6 @@ class AbstractApp(object):
:type arguments: dict | None :type arguments: dict | None
:param auth_all_paths: whether to authenticate not defined paths :param auth_all_paths: whether to authenticate not defined paths
:type auth_all_paths: bool :type auth_all_paths: bool
:param swagger_json: whether to include swagger json or not
:type swagger_json: bool
:param swagger_ui: whether to include swagger ui or not
:type swagger_ui: bool
:param swagger_path: path to swagger-ui directory
:type swagger_path: string | None
:param swagger_url: URL to access swagger-ui documentation
:type swagger_url: string | None
:param validate_responses: True enables validation. Validation errors generate HTTP 500 responses. :param validate_responses: True enables validation. Validation errors generate HTTP 500 responses.
:type validate_responses: bool :type validate_responses: bool
:param strict_validation: True enables validation on invalid request parameters :param strict_validation: True enables validation on invalid request parameters
@@ -128,6 +113,11 @@ class AbstractApp(object):
:type resolver_error: int | None :type resolver_error: int | None
:param pythonic_params: When True CamelCase parameters are converted to snake_case :param pythonic_params: When True CamelCase parameters are converted to snake_case
:type pythonic_params: bool :type pythonic_params: bool
:param options: New style options dictionary.
:type options: dict | None
:param old_style_options: Old style options support for backward compatibility. Preference is
what is defined in `options` parameter.
:type old_style_options: dict
:rtype: AbstractAPI :rtype: AbstractAPI
""" """
# Turn the resolver_error code into a handler object # Turn the resolver_error code into a handler object
@@ -138,12 +128,8 @@ class AbstractApp(object):
resolver = Resolver(resolver) if hasattr(resolver, '__call__') else resolver resolver = Resolver(resolver) if hasattr(resolver, '__call__') else resolver
swagger_json = swagger_json if swagger_json is not None else self.swagger_json
swagger_ui = swagger_ui if swagger_ui is not None else self.swagger_ui
swagger_path = swagger_path if swagger_path is not None else self.swagger_path
swagger_url = swagger_url if swagger_url is not None else self.swagger_url
auth_all_paths = auth_all_paths if auth_all_paths is not None else self.auth_all_paths auth_all_paths = auth_all_paths if auth_all_paths is not None else self.auth_all_paths
# TODO test if base_url starts with an / (if not none) # TODO test if base_path starts with an / (if not none)
arguments = arguments or dict() arguments = arguments or dict()
arguments = dict(self.arguments, **arguments) # copy global arguments and update with api specfic arguments = dict(self.arguments, **arguments) # copy global arguments and update with api specfic
@@ -152,12 +138,16 @@ class AbstractApp(object):
else: else:
specification = self.specification_dir / specification specification = self.specification_dir / specification
api = self.api_cls(specification=specification, # Old style options have higher priority compared to the already
base_url=base_path, arguments=arguments, # defined options in the App class
swagger_json=swagger_json, api_options = self.options.extend(old_style_options)
swagger_ui=swagger_ui,
swagger_path=swagger_path, # locally defined options are added last to preserve highest priority
swagger_url=swagger_url, api_options = api_options.extend(options)
api = self.api_cls(specification,
base_path=base_path,
arguments=arguments,
resolver=resolver, resolver=resolver,
resolver_error_handler=resolver_error_handler, resolver_error_handler=resolver_error_handler,
validate_responses=validate_responses, validate_responses=validate_responses,
@@ -165,7 +155,8 @@ class AbstractApp(object):
auth_all_paths=auth_all_paths, auth_all_paths=auth_all_paths,
debug=self.debug, debug=self.debug,
validator_map=self.validator_map, validator_map=self.validator_map,
pythonic_params=pythonic_params) pythonic_params=pythonic_params,
options=api_options.as_dict())
return api return api
def _resolver_error_handler(self, *args, **kwargs): def _resolver_error_handler(self, *args, **kwargs):

View File

@@ -2,6 +2,7 @@ import datetime
import logging import logging
import pathlib import pathlib
from decimal import Decimal from decimal import Decimal
from types import FunctionType # NOQA
import flask import flask
import werkzeug.exceptions import werkzeug.exceptions
@@ -10,25 +11,14 @@ from flask import json
from ..apis.flask_api import FlaskApi from ..apis.flask_api import FlaskApi
from ..exceptions import ProblemException from ..exceptions import ProblemException
from ..problem import problem from ..problem import problem
from ..resolver import Resolver
from .abstract import AbstractApp from .abstract import AbstractApp
logger = logging.getLogger('connexion.app') logger = logging.getLogger('connexion.app')
class FlaskApp(AbstractApp): class FlaskApp(AbstractApp):
def __init__(self, import_name, port=None, specification_dir='', def __init__(self, import_name, **kwargs):
server=None, arguments=None, auth_all_paths=False, super(FlaskApp, self).__init__(import_name, FlaskApi, server='flask', **kwargs)
debug=False, swagger_json=True, swagger_ui=True, swagger_path=None,
swagger_url=None, host=None, validator_map=None):
server = server or 'flask'
super(FlaskApp, self).__init__(
import_name, port=port, specification_dir=specification_dir,
server=server, arguments=arguments, auth_all_paths=auth_all_paths,
debug=debug, swagger_json=swagger_json, swagger_ui=swagger_ui,
swagger_path=swagger_path, swagger_url=swagger_url,
host=host, validator_map=validator_map, api_cls=FlaskApi
)
def create_app(self): def create_app(self):
app = flask.Flask(self.import_name) app = flask.Flask(self.import_name)
@@ -60,27 +50,13 @@ class FlaskApp(AbstractApp):
return FlaskApi.get_response(response) return FlaskApi.get_response(response)
def add_api(self, specification, base_path=None, arguments=None, def add_api(self, specification, **kwargs):
auth_all_paths=None, swagger_json=None, swagger_ui=None, api = super(FlaskApp, self).add_api(specification, **kwargs)
swagger_path=None, swagger_url=None, validate_responses=False,
strict_validation=False, resolver=Resolver(), resolver_error=None,
pythonic_params=False):
api = super(FlaskApp, self).add_api(
specification, base_path=base_path,
arguments=arguments, auth_all_paths=auth_all_paths, swagger_json=swagger_json,
swagger_ui=swagger_ui, swagger_path=swagger_path, swagger_url=swagger_url,
validate_responses=validate_responses, strict_validation=strict_validation,
resolver=resolver, resolver_error=resolver_error, pythonic_params=pythonic_params
)
self.app.register_blueprint(api.blueprint) self.app.register_blueprint(api.blueprint)
return api return api
def add_error_handler(self, error_code, function): def add_error_handler(self, error_code, function):
""" # type: (int, FunctionType) -> None
:type error_code: int
:type function: types.FunctionType
"""
self.app.register_error_handler(error_code, function) self.app.register_error_handler(error_code, function)
def run(self, port=None, server=None, debug=None, host=None, **options): # pragma: no cover def run(self, port=None, server=None, debug=None, host=None, **options): # pragma: no cover

View File

@@ -401,7 +401,7 @@ class Operation(SecureOperation):
If the operation mimetype format is json then the function return value is jsonified If the operation mimetype format is json then the function return value is jsonified
From Swagger Specfication: From Swagger Specification:
**Produces** **Produces**
@@ -416,8 +416,8 @@ class Operation(SecureOperation):
mimetype = self.get_mimetype() mimetype = self.get_mimetype()
if all_json(self.produces): # endpoint will return json if all_json(self.produces): # endpoint will return json
logger.debug('... Produces json', extra=vars(self)) logger.debug('... Produces json', extra=vars(self))
jsonify = self.api.jsonifier(mimetype) # TODO: Refactor this.
return jsonify return lambda f: f
elif len(self.produces) == 1: elif len(self.produces) == 1:
logger.debug('... Produces %s', mimetype, extra=vars(self)) logger.debug('... Produces %s', mimetype, extra=vars(self))
@@ -453,8 +453,9 @@ class Operation(SecureOperation):
def json_loads(self, data): def json_loads(self, data):
""" """
A Wrapper for calling the jsonifier. A wrapper for calling the API specific JSON loader.
:param data: The json to loads
:param data: The JSON data in textual form.
:type data: bytes :type data: bytes
""" """
return self.api.jsonifier.loads(data) return self.api.json_loads(data)

87
connexion/options.py Normal file
View File

@@ -0,0 +1,87 @@
import pathlib
from typing import Optional # NOQA
MODULE_PATH = pathlib.Path(__file__).absolute().parent
INTERNAL_CONSOLE_UI_PATH = MODULE_PATH / 'vendor' / 'swagger-ui'
class ConnexionOptions(object):
def __init__(self, options=None):
self._options = {}
if options:
self._options.update(filter_values(options))
def extend(self, new_values=None):
# type: (Optional[dict]) -> ConnexionOptions
"""
Return a new instance of `ConnexionOptions` using as default the currently
defined options.
"""
if new_values is None:
new_values = {}
options = dict(self._options)
options.update(filter_values(new_values))
return ConnexionOptions(options)
def as_dict(self):
return self._options
@property
def openapi_spec_available(self):
# type: () -> bool
"""
Whether to make available the OpenAPI Specification under
`openapi_console_ui_path`/swagger.json path.
Default: True
"""
# NOTE: Under OpenAPI v3 this should change to "/openapi.json"
return self._options.get('swagger_json', True)
@property
def openapi_console_ui_available(self):
# type: () -> bool
"""
Whether to make the OpenAPI Console UI available under the path
defined in `openapi_console_ui_path` option. Note that if enabled,
this overrides the `openapi_spec_available` option since the specification
is required to be available via a HTTP endpoint to display the console UI.
Default: True
"""
return self._options.get('swagger_ui', True)
@property
def openapi_console_ui_path(self):
# type: () -> str
"""
Path to mount the OpenAPI Console UI and make it accessible via a browser.
Default: /ui
"""
return self._options.get('swagger_url', '/ui')
@property
def openapi_console_ui_from_dir(self):
# type: () -> str
"""
Custom OpenAPI Console UI directory from where Connexion will serve
the static files.
Default: Connexion's vendored version of the OpenAPI Console UI.
"""
return self._options.get('swagger_path', INTERNAL_CONSOLE_UI_PATH)
def filter_values(dictionary):
# type: (dict) -> dict
"""
Remove `None` value entries in the dictionary.
:param dictionary:
:return:
"""
return dict([(key, value)
for key, value in dictionary.items()
if value is not None])

View File

@@ -31,7 +31,8 @@ install_requires = [
'requests>=2.9.1', 'requests>=2.9.1',
'six>=1.9', 'six>=1.9',
'swagger-spec-validator>=2.0.2', 'swagger-spec-validator>=2.0.2',
'inflection>=0.3.1' 'inflection>=0.3.1',
'typing>=3.6.1'
] ]
flask_require = 'flask>=0.10.1' flask_require = 'flask>=0.10.1'

View File

@@ -3,13 +3,14 @@ import yaml
import pytest import pytest
from conftest import TEST_FOLDER, build_app_from_fixture from conftest import TEST_FOLDER, build_app_from_fixture
from connexion import FlaskApp from connexion import App
from connexion.exceptions import InvalidSpecification from connexion.exceptions import InvalidSpecification
def test_app_with_relative_path(simple_api_spec_dir): def test_app_with_relative_path(simple_api_spec_dir):
# Create the app with a realative path and run the test_app testcase below. # Create the app with a relative path and run the test_app testcase below.
app = FlaskApp(__name__, 5001, '..' / simple_api_spec_dir.relative_to(TEST_FOLDER), app = App(__name__, port=5001,
specification_dir='..' / simple_api_spec_dir.relative_to(TEST_FOLDER),
debug=True) debug=True)
app.add_api('swagger.yaml') app.add_api('swagger.yaml')
@@ -20,14 +21,15 @@ def test_app_with_relative_path(simple_api_spec_dir):
def test_no_swagger_ui(simple_api_spec_dir): def test_no_swagger_ui(simple_api_spec_dir):
app = FlaskApp(__name__, 5001, simple_api_spec_dir, swagger_ui=False, debug=True) app = App(__name__, port=5001, specification_dir=simple_api_spec_dir,
swagger_ui=False, debug=True)
app.add_api('swagger.yaml') app.add_api('swagger.yaml')
app_client = app.app.test_client() app_client = app.app.test_client()
swagger_ui = app_client.get('/v1.0/ui/') # type: flask.Response swagger_ui = app_client.get('/v1.0/ui/') # type: flask.Response
assert swagger_ui.status_code == 404 assert swagger_ui.status_code == 404
app2 = FlaskApp(__name__, 5001, simple_api_spec_dir, debug=True) app2 = App(__name__, port=5001, specification_dir=simple_api_spec_dir, debug=True)
app2.add_api('swagger.yaml', swagger_ui=False) app2.add_api('swagger.yaml', swagger_ui=False)
app2_client = app2.app.test_client() app2_client = app2.app.test_client()
swagger_ui2 = app2_client.get('/v1.0/ui/') # type: flask.Response swagger_ui2 = app2_client.get('/v1.0/ui/') # type: flask.Response
@@ -36,7 +38,7 @@ def test_no_swagger_ui(simple_api_spec_dir):
def test_swagger_json_app(simple_api_spec_dir): def test_swagger_json_app(simple_api_spec_dir):
""" Verify the swagger.json file is returned for default setting passed to app. """ """ Verify the swagger.json file is returned for default setting passed to app. """
app = FlaskApp(__name__, 5001, simple_api_spec_dir, debug=True) app = App(__name__, port=5001, specification_dir=simple_api_spec_dir, debug=True)
app.add_api('swagger.yaml') app.add_api('swagger.yaml')
app_client = app.app.test_client() app_client = app.app.test_client()
@@ -46,7 +48,8 @@ def test_swagger_json_app(simple_api_spec_dir):
def test_no_swagger_json_app(simple_api_spec_dir): def test_no_swagger_json_app(simple_api_spec_dir):
""" Verify the swagger.json file is not returned when set to False when creating app. """ """ Verify the swagger.json file is not returned when set to False when creating app. """
app = FlaskApp(__name__, 5001, simple_api_spec_dir, swagger_json=False, debug=True) app = App(__name__, port=5001, specification_dir=simple_api_spec_dir,
swagger_json=False, debug=True)
app.add_api('swagger.yaml') app.add_api('swagger.yaml')
app_client = app.app.test_client() app_client = app.app.test_client()
@@ -55,7 +58,6 @@ def test_no_swagger_json_app(simple_api_spec_dir):
def test_dict_as_yaml_path(simple_api_spec_dir): def test_dict_as_yaml_path(simple_api_spec_dir):
swagger_yaml_path = simple_api_spec_dir / 'swagger.yaml' swagger_yaml_path = simple_api_spec_dir / 'swagger.yaml'
with swagger_yaml_path.open(mode='rb') as swagger_yaml: with swagger_yaml_path.open(mode='rb') as swagger_yaml:
@@ -68,7 +70,7 @@ def test_dict_as_yaml_path(simple_api_spec_dir):
swagger_string = jinja2.Template(swagger_template).render({}) swagger_string = jinja2.Template(swagger_template).render({})
specification = yaml.safe_load(swagger_string) # type: dict specification = yaml.safe_load(swagger_string) # type: dict
app = FlaskApp(__name__, 5001, simple_api_spec_dir, debug=True) app = App(__name__, port=5001, specification_dir=simple_api_spec_dir, debug=True)
app.add_api(specification) app.add_api(specification)
app_client = app.app.test_client() app_client = app.app.test_client()
@@ -78,7 +80,7 @@ def test_dict_as_yaml_path(simple_api_spec_dir):
def test_swagger_json_api(simple_api_spec_dir): def test_swagger_json_api(simple_api_spec_dir):
""" Verify the swagger.json file is returned for default setting passed to api. """ """ Verify the swagger.json file is returned for default setting passed to api. """
app = FlaskApp(__name__, 5001, simple_api_spec_dir, debug=True) app = App(__name__, port=5001, specification_dir=simple_api_spec_dir, debug=True)
app.add_api('swagger.yaml') app.add_api('swagger.yaml')
app_client = app.app.test_client() app_client = app.app.test_client()
@@ -88,7 +90,7 @@ def test_swagger_json_api(simple_api_spec_dir):
def test_no_swagger_json_api(simple_api_spec_dir): def test_no_swagger_json_api(simple_api_spec_dir):
""" Verify the swagger.json file is not returned when set to False when adding api. """ """ Verify the swagger.json file is not returned when set to False when adding api. """
app = FlaskApp(__name__, 5001, simple_api_spec_dir, debug=True) app = App(__name__, port=5001, specification_dir=simple_api_spec_dir, debug=True)
app.add_api('swagger.yaml', swagger_json=False) app.add_api('swagger.yaml', swagger_json=False)
app_client = app.app.test_client() app_client = app.app.test_client()
@@ -143,7 +145,7 @@ def test_resolve_classmethod(simple_app):
def test_add_api_with_function_resolver_function_is_wrapped(simple_api_spec_dir): def test_add_api_with_function_resolver_function_is_wrapped(simple_api_spec_dir):
app = FlaskApp(__name__, specification_dir=simple_api_spec_dir) app = App(__name__, specification_dir=simple_api_spec_dir)
api = app.add_api('swagger.yaml', resolver=lambda oid: (lambda foo: 'bar')) api = app.add_api('swagger.yaml', resolver=lambda oid: (lambda foo: 'bar'))
assert api.resolver.resolve_function_from_operation_id('faux')('bah') == 'bar' assert api.resolver.resolve_function_from_operation_id('faux')('bah') == 'bar'

View File

@@ -8,18 +8,21 @@ def test_app(simple_app):
assert simple_app.port == 5001 assert simple_app.port == 5001
app_client = simple_app.app.test_client() app_client = simple_app.app.test_client()
# by default the Swagger UI is enabled
swagger_ui = app_client.get('/v1.0/ui/') # type: flask.Response swagger_ui = app_client.get('/v1.0/ui/') # type: flask.Response
assert swagger_ui.status_code == 200 assert swagger_ui.status_code == 200
assert b"Swagger UI" in swagger_ui.data assert b"Swagger UI" in swagger_ui.data
# test return Swagger UI static files
swagger_icon = app_client.get('/v1.0/ui/images/favicon.ico') # type: flask.Response swagger_icon = app_client.get('/v1.0/ui/images/favicon.ico') # type: flask.Response
assert swagger_icon.status_code == 200 assert swagger_icon.status_code == 200
post_greeting = app_client.post('/v1.0/greeting/jsantos', data={}) # type: flask.Response post_greeting = app_client.post('/v1.0/greeting/jsantos', data={}) # type: flask.Response
assert post_greeting.status_code == 200 assert post_greeting.status_code == 200
assert post_greeting.content_type == 'application/json' assert post_greeting.content_type == 'application/json'
greeting_reponse = json.loads(post_greeting.data.decode('utf-8')) greeting_response = json.loads(post_greeting.data.decode('utf-8'))
assert greeting_reponse['greeting'] == 'Hello jsantos' assert greeting_response['greeting'] == 'Hello jsantos'
get_bye = app_client.get('/v1.0/bye/jsantos') # type: flask.Response get_bye = app_client.get('/v1.0/bye/jsantos') # type: flask.Response
assert get_bye.status_code == 200 assert get_bye.status_code == 200
@@ -28,8 +31,8 @@ def test_app(simple_app):
post_greeting = app_client.post('/v1.0/greeting/jsantos', data={}) # type: flask.Response post_greeting = app_client.post('/v1.0/greeting/jsantos', data={}) # type: flask.Response
assert post_greeting.status_code == 200 assert post_greeting.status_code == 200
assert post_greeting.content_type == 'application/json' assert post_greeting.content_type == 'application/json'
greeting_reponse = json.loads(post_greeting.data.decode('utf-8')) greeting_response = json.loads(post_greeting.data.decode('utf-8'))
assert greeting_reponse['greeting'] == 'Hello jsantos' assert greeting_response['greeting'] == 'Hello jsantos'
def test_produce_decorator(simple_app): def test_produce_decorator(simple_app):

View File

@@ -4,8 +4,8 @@ from connexion import FlaskApp
def test_security_over_inexistent_endpoints(oauth_requests, secure_api_spec_dir): def test_security_over_inexistent_endpoints(oauth_requests, secure_api_spec_dir):
app1 = FlaskApp(__name__, 5001, secure_api_spec_dir, swagger_ui=False, app1 = FlaskApp(__name__, port=5001, specification_dir=secure_api_spec_dir,
debug=True, auth_all_paths=True) swagger_ui=False, debug=True, auth_all_paths=True)
app1.add_api('swagger.yaml') app1.add_api('swagger.yaml')
assert app1.port == 5001 assert app1.port == 5001
@@ -74,7 +74,8 @@ def test_security(oauth_requests, secure_endpoint_app):
assert response.status_code == 200 assert response.status_code == 200
headers = {"Authorization": "Bearer 100"} headers = {"Authorization": "Bearer 100"}
get_bye_good_auth = app_client.get('/v1.0/byesecure-ignoring-context/hjacobs', headers=headers) # type: flask.Response get_bye_good_auth = app_client.get('/v1.0/byesecure-ignoring-context/hjacobs',
headers=headers) # type: flask.Response
assert get_bye_good_auth.status_code == 200 assert get_bye_good_auth.status_code == 200
assert get_bye_good_auth.data == b'Goodbye hjacobs (Secure!)' assert get_bye_good_auth.data == b'Goodbye hjacobs (Secure!)'

View File

@@ -3,7 +3,7 @@ import logging
import pathlib import pathlib
import pytest import pytest
from connexion import FlaskApi, FlaskApp from connexion import App
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
@@ -55,9 +55,9 @@ def oauth_requests(monkeypatch):
@pytest.fixture @pytest.fixture
def app(): def app():
app = FlaskApp(__name__, 5001, SPEC_FOLDER, debug=True) cnx_app = App(__name__, port=5001, specification_dir=SPEC_FOLDER, debug=True)
app.add_api('api.yaml', validate_responses=True) cnx_app.add_api('api.yaml', validate_responses=True)
return app return cnx_app
@pytest.fixture @pytest.fixture
@@ -85,9 +85,13 @@ def build_app_from_fixture(api_spec_folder, **kwargs):
if 'debug' in kwargs: if 'debug' in kwargs:
debug = kwargs['debug'] debug = kwargs['debug']
del (kwargs['debug']) del (kwargs['debug'])
app = FlaskApp(__name__, 5001, FIXTURES_FOLDER / api_spec_folder, debug=debug)
app.add_api('swagger.yaml', **kwargs) cnx_app = App(__name__,
return app port=5001,
specification_dir=FIXTURES_FOLDER / api_spec_folder,
debug=debug)
cnx_app.add_api('swagger.yaml', **kwargs)
return cnx_app
@pytest.fixture(scope="session") @pytest.fixture(scope="session")

View File

@@ -8,112 +8,113 @@ from yaml import YAMLError
import pytest import pytest
from connexion import FlaskApi from connexion import FlaskApi
from connexion.apis.abstract import canonical_base_url from connexion.apis.abstract import canonical_base_path
from connexion.exceptions import InvalidSpecification, ResolverError from connexion.exceptions import InvalidSpecification, ResolverError
from mock import MagicMock
TEST_FOLDER = pathlib.Path(__file__).parent TEST_FOLDER = pathlib.Path(__file__).parent
def test_canonical_base_url(): def test_canonical_base_path():
assert canonical_base_url('') == '' assert canonical_base_path('') == ''
assert canonical_base_url('/') == '' assert canonical_base_path('/') == ''
assert canonical_base_url('/api') == '/api' assert canonical_base_path('/api') == '/api'
assert canonical_base_url('/api/') == '/api' assert canonical_base_path('/api/') == '/api'
def test_api(): def test_api():
api = FlaskApi(TEST_FOLDER / "fixtures/simple/swagger.yaml", "/api/v1.0", {}) api = FlaskApi(TEST_FOLDER / "fixtures/simple/swagger.yaml", base_path="/api/v1.0")
assert api.blueprint.name == '/api/v1_0' assert api.blueprint.name == '/api/v1_0'
assert api.blueprint.url_prefix == '/api/v1.0' assert api.blueprint.url_prefix == '/api/v1.0'
# TODO test base_url in spec
api2 = FlaskApi(TEST_FOLDER / "fixtures/simple/swagger.yaml") api2 = FlaskApi(TEST_FOLDER / "fixtures/simple/swagger.yaml")
assert api2.blueprint.name == '/v1_0' assert api2.blueprint.name == '/v1_0'
assert api2.blueprint.url_prefix == '/v1.0' assert api2.blueprint.url_prefix == '/v1.0'
def test_api_basepath_slash(): def test_api_base_path_slash():
api = FlaskApi(TEST_FOLDER / "fixtures/simple/basepath-slash.yaml", None, {}) api = FlaskApi(TEST_FOLDER / "fixtures/simple/basepath-slash.yaml")
assert api.blueprint.name == '' assert api.blueprint.name == ''
assert api.blueprint.url_prefix == '' assert api.blueprint.url_prefix == ''
def test_template(): def test_template():
api1 = FlaskApi(TEST_FOLDER / "fixtures/simple/swagger.yaml", "/api/v1.0", {'title': 'test'}) api1 = FlaskApi(TEST_FOLDER / "fixtures/simple/swagger.yaml",
base_path="/api/v1.0", arguments={'title': 'test'})
assert api1.specification['info']['title'] == 'test' assert api1.specification['info']['title'] == 'test'
api2 = FlaskApi(TEST_FOLDER / "fixtures/simple/swagger.yaml", "/api/v1.0", {'title': 'other test'}) api2 = FlaskApi(TEST_FOLDER / "fixtures/simple/swagger.yaml",
base_path="/api/v1.0", arguments={'title': 'other test'})
assert api2.specification['info']['title'] == 'other test' assert api2.specification['info']['title'] == 'other test'
def test_invalid_operation_does_stop_application_to_setup(): def test_invalid_operation_does_stop_application_to_setup():
with pytest.raises(ImportError): with pytest.raises(ImportError):
FlaskApi(TEST_FOLDER / "fixtures/op_error_api/swagger.yaml", "/api/v1.0", FlaskApi(TEST_FOLDER / "fixtures/op_error_api/swagger.yaml",
{'title': 'OK'}) base_path="/api/v1.0", arguments={'title': 'OK'})
with pytest.raises(ResolverError): with pytest.raises(ResolverError):
FlaskApi(TEST_FOLDER / "fixtures/missing_op_id/swagger.yaml", "/api/v1.0", FlaskApi(TEST_FOLDER / "fixtures/missing_op_id/swagger.yaml",
{'title': 'OK'}) base_path="/api/v1.0", arguments={'title': 'OK'})
with pytest.raises(ImportError): with pytest.raises(ImportError):
FlaskApi(TEST_FOLDER / "fixtures/module_not_implemented/swagger.yaml", "/api/v1.0", FlaskApi(TEST_FOLDER / "fixtures/module_not_implemented/swagger.yaml",
{'title': 'OK'}) base_path="/api/v1.0", arguments={'title': 'OK'})
with pytest.raises(ValueError): with pytest.raises(ValueError):
FlaskApi(TEST_FOLDER / "fixtures/user_module_loading_error/swagger.yaml", "/api/v1.0", FlaskApi(TEST_FOLDER / "fixtures/user_module_loading_error/swagger.yaml",
{'title': 'OK'}) base_path="/api/v1.0", arguments={'title': 'OK'})
with pytest.raises(ResolverError): with pytest.raises(ResolverError):
FlaskApi(TEST_FOLDER / "fixtures/missing_op_id/swagger.yaml", "/api/v1.0", FlaskApi(TEST_FOLDER / "fixtures/missing_op_id/swagger.yaml",
{'title': 'OK'}) base_path="/api/v1.0", arguments={'title': 'OK'})
def test_invalid_operation_does_not_stop_application_in_debug_mode(): def test_invalid_operation_does_not_stop_application_in_debug_mode():
api = FlaskApi(TEST_FOLDER / "fixtures/op_error_api/swagger.yaml", "/api/v1.0", api = FlaskApi(TEST_FOLDER / "fixtures/op_error_api/swagger.yaml",
{'title': 'OK'}, debug=True) base_path="/api/v1.0", arguments={'title': 'OK'}, debug=True)
assert api.specification['info']['title'] == 'OK' assert api.specification['info']['title'] == 'OK'
api = FlaskApi(TEST_FOLDER / "fixtures/missing_op_id/swagger.yaml", "/api/v1.0", api = FlaskApi(TEST_FOLDER / "fixtures/missing_op_id/swagger.yaml",
{'title': 'OK'}, debug=True) base_path="/api/v1.0", arguments={'title': 'OK'}, debug=True)
assert api.specification['info']['title'] == 'OK' assert api.specification['info']['title'] == 'OK'
api = FlaskApi(TEST_FOLDER / "fixtures/module_not_implemented/swagger.yaml", "/api/v1.0", api = FlaskApi(TEST_FOLDER / "fixtures/module_not_implemented/swagger.yaml",
{'title': 'OK'}, debug=True) base_path="/api/v1.0", arguments={'title': 'OK'}, debug=True)
assert api.specification['info']['title'] == 'OK' assert api.specification['info']['title'] == 'OK'
api = FlaskApi(TEST_FOLDER / "fixtures/user_module_loading_error/swagger.yaml", "/api/v1.0", api = FlaskApi(TEST_FOLDER / "fixtures/user_module_loading_error/swagger.yaml",
{'title': 'OK'}, debug=True) base_path="/api/v1.0", arguments={'title': 'OK'}, debug=True)
assert api.specification['info']['title'] == 'OK' assert api.specification['info']['title'] == 'OK'
api = FlaskApi(TEST_FOLDER / "fixtures/missing_op_id/swagger.yaml", "/api/v1.0", api = FlaskApi(TEST_FOLDER / "fixtures/missing_op_id/swagger.yaml",
{'title': 'OK'}, debug=True) base_path="/api/v1.0", arguments={'title': 'OK'}, debug=True)
assert api.specification['info']['title'] == 'OK' assert api.specification['info']['title'] == 'OK'
def test_other_errors_stop_application_to_setup(): def test_other_errors_stop_application_to_setup():
# The previous tests were just about operationId not being resolvable. # Errors should still result exceptions!
# Other errors should still result exceptions!
with pytest.raises(InvalidSpecification): with pytest.raises(InvalidSpecification):
FlaskApi(TEST_FOLDER / "fixtures/bad_specs/swagger.yaml", "/api/v1.0", FlaskApi(TEST_FOLDER / "fixtures/bad_specs/swagger.yaml",
{'title': 'OK'}) base_path="/api/v1.0", arguments={'title': 'OK'})
# Debug mode should ignore the error # Debug mode should ignore the error
api = FlaskApi(TEST_FOLDER / "fixtures/bad_specs/swagger.yaml", "/api/v1.0", api = FlaskApi(TEST_FOLDER / "fixtures/bad_specs/swagger.yaml",
{'title': 'OK'}, debug=True) base_path="/api/v1.0", arguments={'title': 'OK'}, debug=True)
assert api.specification['info']['title'] == 'OK' assert api.specification['info']['title'] == 'OK'
def test_invalid_schema_file_structure(): def test_invalid_schema_file_structure():
with pytest.raises(SwaggerValidationError): with pytest.raises(SwaggerValidationError):
FlaskApi(TEST_FOLDER / "fixtures/invalid_schema/swagger.yaml", "/api/v1.0", FlaskApi(TEST_FOLDER / "fixtures/invalid_schema/swagger.yaml",
{'title': 'OK'}, debug=True) base_path="/api/v1.0", arguments={'title': 'OK'}, debug=True)
def test_invalid_encoding(): def test_invalid_encoding():
with tempfile.NamedTemporaryFile(mode='wb') as f: with tempfile.NamedTemporaryFile(mode='wb') as f:
f.write(u"swagger: '2.0'\ninfo:\n title: Foo 整\n version: v1\npaths: {}".encode('gbk')) f.write(u"swagger: '2.0'\ninfo:\n title: Foo 整\n version: v1\npaths: {}".encode('gbk'))
f.flush() f.flush()
FlaskApi(pathlib.Path(f.name), "/api/v1.0") FlaskApi(pathlib.Path(f.name), base_path="/api/v1.0")
def test_use_of_safe_load_for_yaml_swagger_specs(): def test_use_of_safe_load_for_yaml_swagger_specs():
@@ -122,7 +123,7 @@ def test_use_of_safe_load_for_yaml_swagger_specs():
f.write('!!python/object:object {}\n'.encode()) f.write('!!python/object:object {}\n'.encode())
f.flush() f.flush()
try: try:
FlaskApi(pathlib.Path(f.name), "/api/v1.0") FlaskApi(pathlib.Path(f.name), base_path="/api/v1.0")
except SwaggerValidationError: except SwaggerValidationError:
pytest.fail("Could load invalid YAML file, use yaml.safe_load!") pytest.fail("Could load invalid YAML file, use yaml.safe_load!")
@@ -132,4 +133,17 @@ def test_validation_error_on_completely_invalid_swagger_spec():
with tempfile.NamedTemporaryFile() as f: with tempfile.NamedTemporaryFile() as f:
f.write('[1]\n'.encode()) f.write('[1]\n'.encode())
f.flush() f.flush()
FlaskApi(pathlib.Path(f.name), "/api/v1.0") FlaskApi(pathlib.Path(f.name), base_path="/api/v1.0")
@pytest.fixture
def mock_api_logger(monkeypatch):
mocked_logger = MagicMock(name='mocked_logger')
monkeypatch.setattr('connexion.apis.abstract.logger', mocked_logger)
return mocked_logger
def test_warn_users_about_base_url_parameter_name_change(mock_api_logger):
FlaskApi(TEST_FOLDER / "fixtures/simple/swagger.yaml", base_url="/api/v1")
mock_api_logger.warning.assert_called_with(
'Parameter base_url should be no longer used. Use base_path instead.')