Skip to content

Commit

Permalink
Merge 32a9ca5 into b6bb9dd
Browse files Browse the repository at this point in the history
  • Loading branch information
jmcs committed Sep 3, 2018
2 parents b6bb9dd + 32a9ca5 commit 8abe554
Show file tree
Hide file tree
Showing 94 changed files with 1,929 additions and 39,113 deletions.
39 changes: 33 additions & 6 deletions README.rst
Expand Up @@ -77,6 +77,19 @@ Other Sources/Mentions
- Connexion listed on Swagger_'s website
- Blog post: `Crafting effective Microservices in Python`_

New in Connexion 2.0:
---------------------
- App and Api options must be provided through the "options" argument (``old_style_options`` have been removed).
- The `Operation` interface has been formalized in the `AbstractOperation` class.
- The `Operation` class has been renamed to `Swagger2Operation`.
- Array parameter deserialization now follows the Swagger 2.0 spec more closely.
In situations when a query parameter is passed multiple times, and the collectionFormat is either csv or pipes, the right-most value will be used.
For example, `?q=1,2,3&q=4,5,6` will result in `q = [4, 5, 6]`.
The old behavior is available by setting the collectionFormat to `multi`, or by importing `decorators.uri_parsing.AlwaysMultiURIParser` and passing `parser_class=AlwaysMultiURIParser` to your Api.
- The spec validator library has changed from `swagger-spec-validator` to `openapi-spec-validator`.
- Errors that previously raised `SwaggerValidationError` now raise the `InvalidSpecification` exception.
All spec validation errors should be wrapped with `InvalidSpecification`.

How to Use
==========

Expand Down Expand Up @@ -398,8 +411,10 @@ parameters to the underlying `werkzeug`_ server.
The Swagger UI Console
----------------------

The Swagger UI for an API is available, by default, in
``{base_path}/ui/`` where ``base_path`` is the base path of the API.
The Swagger UI for an API is available through pip extras.
You can install it with ``pip install connexion[swagger-ui]``.
It will be served up at ``{base_path}/ui/`` where ``base_path`` is the
base path of the API.

You can disable the Swagger UI at the application level:

Expand All @@ -418,20 +433,32 @@ You can also disable it at the API level:
app.add_api('my_api.yaml', swagger_ui=False)
If necessary, you can explicitly specify the path to the directory with
swagger-ui to not use the connexion-embedded swagger-ui distro.
swagger-ui to not use the connexion[swagger-ui] distro.
In order to do this, you should specify the following option:

.. code-block:: python
options = {'swagger_path': '/path/to/swagger_ui/'}
app = connexion.App(__name__, specification_dir='swagger/', options=options)
Make sure that ``swagger_ui/index.html`` loads by default local swagger json.
You can use the ``api_url`` jinja variable for this purpose:
If you wish to provide your own swagger-ui distro, note that connextion
expects a jinja2 file called ``swagger_ui/index.j2`` in order to load the
correct ``swagger.json`` by default. Your ``index.j2`` file can use the
``openapi_spec_url`` jinja variable for this purpose:

.. code-block::
const ui = SwaggerUIBundle({ url: "{{ api_url }}/swagger.json"})
const ui = SwaggerUIBundle({ url: "{{ openapi_spec_url }}"})
Additionally, if you wish to use swagger-ui-3.x.x, it is also provided by
installing connexion[swagger-ui], and can be enabled like this:

.. code-block:: python
from swagger_ui_bundle import swagger_ui_3_path
options = {'swagger_path': swagger_ui_3_path}
app = connexion.App(__name__, specification_dir='swagger/', options=options)
Server Backend
--------------
Expand Down
100 changes: 43 additions & 57 deletions connexion/apis/abstract.py
Expand Up @@ -8,20 +8,19 @@
import jinja2
import six
import yaml
from swagger_spec_validator.validator20 import validate_spec
from openapi_spec_validator import validate_v2_spec as validate_spec
from openapi_spec_validator.exceptions import OpenAPIValidationError

from ..exceptions import ResolverError
from ..operation import Operation
from ..exceptions import InvalidSpecification, ResolverError
from ..jsonref import resolve_refs
from ..operations import Swagger2Operation
from ..options import ConnexionOptions
from ..resolver import Resolver
from ..utils import Jsonifier

MODULE_PATH = pathlib.Path(__file__).absolute().parent.parent
SWAGGER_UI_PATH = MODULE_PATH / 'vendor' / 'swagger-ui'
SWAGGER_UI_URL = 'ui'

RESOLVER_ERROR_ENDPOINT_RANDOM_DIGITS = 6

logger = logging.getLogger('connexion.apis.abstract')


Expand All @@ -41,8 +40,7 @@ class AbstractAPI(object):
def __init__(self, specification, base_path=None, arguments=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, options=None, pass_context_arg_name=None,
**old_style_options):
validator_map=None, pythonic_params=False, pass_context_arg_name=None, options=None):
"""
:type specification: pathlib.Path | dict
:type base_path: str | None
Expand All @@ -65,29 +63,22 @@ def __init__(self, specification, base_path=None, arguments=None,
:param pass_context_arg_name: If not None URL request handling functions with an argument matching this name
will be passed the framework's request context.
:type pass_context_arg_name: str | None
:param old_style_options: Old style options support for backward compatibility. Preference is
what is defined in `options` parameter.
"""
self.debug = debug
self.validator_map = validator_map
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)
self.options = ConnexionOptions(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('Options Loaded',
extra={'swagger_ui': self.options.openapi_console_ui_available,
'swagger_path': self.options.openapi_console_ui_from_dir,
'swagger_url': self.options.openapi_console_ui_path})

logger.debug('Loading specification: %s', specification,
extra={'swagger_yaml': specification,
'base_path': base_path,
'arguments': arguments,
'swagger_ui': self.options.openapi_console_ui_available,
'swagger_path': self.options.openapi_console_ui_from_dir,
'swagger_url': self.options.openapi_console_ui_path,
'auth_all_paths': auth_all_paths})

if isinstance(specification, dict):
Expand All @@ -100,8 +91,9 @@ def __init__(self, specification, base_path=None, arguments=None,
logger.debug('Read specification', extra={'spec': self.specification})

# Avoid validator having ability to modify specification
spec = copy.deepcopy(self.specification)
self._validate_spec(spec)
self.raw_spec = copy.deepcopy(self.specification)
self._validate_spec(self.specification)
self.specification = resolve_refs(self.specification)

# https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#fixed-fields
# If base_path is not on provided then we try to read it from the swagger.yaml or use / by default
Expand Down Expand Up @@ -138,7 +130,7 @@ def __init__(self, specification, base_path=None, arguments=None,
self.pass_context_arg_name = pass_context_arg_name

if self.options.openapi_spec_available:
self.add_swagger_json()
self.add_openapi_json()

if self.options.openapi_console_ui_available:
self.add_swagger_ui()
Expand All @@ -149,7 +141,10 @@ def __init__(self, specification, base_path=None, arguments=None,
self.add_auth_on_not_found(self.security, self.security_definitions)

def _validate_spec(self, spec):
validate_spec(spec)
try:
validate_spec(spec)
except OpenAPIValidationError as e:
raise InvalidSpecification.create_from(e)

def _set_base_path(self, base_path):
# type: (AnyStr) -> None
Expand All @@ -160,9 +155,10 @@ def _set_base_path(self, base_path):
self.specification['basePath'] = base_path

@abc.abstractmethod
def add_swagger_json(self):
def add_openapi_json(self):
"""
Adds swagger json to {base_path}/swagger.json
Adds openapi spec to {base_path}/openapi.json
(or {base_path}/swagger.json for swagger2)
"""

@abc.abstractmethod
Expand Down Expand Up @@ -194,25 +190,25 @@ def add_operation(self, method, path, swagger_operation, path_parameters):
:type path: str
:type swagger_operation: dict
"""
operation = Operation(self,
method=method,
path=path,
path_parameters=path_parameters,
operation=swagger_operation,
app_produces=self.produces,
app_consumes=self.consumes,
app_security=self.security,
security_definitions=self.security_definitions,
definitions=self.definitions,
parameter_definitions=self.parameter_definitions,
response_definitions=self.response_definitions,
validate_responses=self.validate_responses,
validator_map=self.validator_map,
strict_validation=self.strict_validation,
resolver=self.resolver,
pythonic_params=self.pythonic_params,
uri_parser_class=self.options.uri_parser_class,
pass_context_arg_name=self.pass_context_arg_name)
operation = Swagger2Operation(self,
method=method,
path=path,
path_parameters=path_parameters,
operation=swagger_operation,
app_produces=self.produces,
app_consumes=self.consumes,
app_security=self.security,
security_definitions=self.security_definitions,
definitions=self.definitions,
parameter_definitions=self.parameter_definitions,
response_definitions=self.response_definitions,
validate_responses=self.validate_responses,
validator_map=self.validator_map,
strict_validation=self.strict_validation,
resolver=self.resolver,
pythonic_params=self.pythonic_params,
uri_parser_class=self.options.uri_parser_class,
pass_context_arg_name=self.pass_context_arg_name)
self._add_operation_internal(method, path, operation)

@abc.abstractmethod
Expand All @@ -227,18 +223,8 @@ def _add_resolver_error_handler(self, method, path, err):
Adds a handler for ResolverError for the given method and path.
"""
operation = self.resolver_error_handler(err,
method=method,
path=path,
app_produces=self.produces,
app_security=self.security,
security_definitions=self.security_definitions,
definitions=self.definitions,
parameter_definitions=self.parameter_definitions,
response_definitions=self.response_definitions,
validate_responses=self.validate_responses,
strict_validation=self.strict_validation,
resolver=self.resolver,
randomize_endpoint=RESOLVER_ERROR_ENDPOINT_RANDOM_DIGITS)
security=self.security,
security_definitions=self.security_definitions)
self._add_operation_internal(method, path, operation)

def add_paths(self, paths=None):
Expand Down
21 changes: 12 additions & 9 deletions connexion/apis/aiohttp_api.py
Expand Up @@ -64,23 +64,25 @@ def _set_base_path(self, base_path):
def normalize_string(string):
return re.sub(r'[^a-zA-Z0-9]', '_', string.strip('/'))

def add_swagger_json(self):
def add_openapi_json(self):
"""
Adds swagger json to {base_path}/swagger.json
Adds openapi json to {base_path}/openapi.json
(or {base_path}/swagger.json for swagger2)
"""
logger.debug('Adding swagger.json: %s/swagger.json', self.base_path)
logger.debug('Adding spec json: %s/%s', self.base_path,
self.options.openapi_spec_path)
self.subapp.router.add_route(
'GET',
'/swagger.json',
self._get_swagger_json
self.options.openapi_spec_path,
self._get_openapi_json
)

@asyncio.coroutine
def _get_swagger_json(self, req):
def _get_openapi_json(self, req):
return web.Response(
status=200,
content_type='application/json',
body=self.jsonifier.dumps(self.specification)
body=self.jsonifier.dumps(self.raw_spec)
)

def add_swagger_ui(self):
Expand Down Expand Up @@ -109,10 +111,11 @@ def add_swagger_ui(self):
name='swagger_ui_static'
)

@aiohttp_jinja2.template('index.html')
@aiohttp_jinja2.template('index.j2')
@asyncio.coroutine
def _get_swagger_ui_home(self, req):
return {'api_url': self.base_path}
return {'openapi_spec_url': (self.base_path +
self.options.openapi_spec_path)}

def add_auth_on_not_found(self, security, security_definitions):
"""
Expand Down
19 changes: 12 additions & 7 deletions connexion/apis/flask_api.py
Expand Up @@ -26,15 +26,17 @@ def _set_blueprint(self):
self.blueprint = flask.Blueprint(endpoint, __name__, url_prefix=self.base_path,
template_folder=str(self.options.openapi_console_ui_from_dir))

def add_swagger_json(self):
def add_openapi_json(self):
"""
Adds swagger json to {base_path}/swagger.json
Adds spec json to {base_path}/swagger.json
or {base_path}/openapi.json (for oas3)
"""
logger.debug('Adding swagger.json: %s/swagger.json', self.base_path)
endpoint_name = "{name}_swagger_json".format(name=self.blueprint.name)
self.blueprint.add_url_rule('/swagger.json',
logger.debug('Adding spec json: %s/%s', self.base_path,
self.options.openapi_spec_path)
endpoint_name = "{name}_openapi_json".format(name=self.blueprint.name)
self.blueprint.add_url_rule(self.options.openapi_spec_path,
endpoint_name,
lambda: flask.jsonify(self.specification))
lambda: flask.jsonify(self.raw_spec))

def add_swagger_ui(self):
"""
Expand Down Expand Up @@ -279,7 +281,10 @@ def console_ui_home(self):
:return:
"""
return flask.render_template('index.html', api_url=self.base_path)
return flask.render_template(
'index.j2',
openapi_spec_url=(self.base_path + self.options.openapi_spec_path)
)

def console_ui_static_files(self, filename):
"""
Expand Down
22 changes: 4 additions & 18 deletions connexion/apps/abstract.py
Expand Up @@ -14,7 +14,7 @@
class AbstractApp(object):
def __init__(self, import_name, api_cls, port=None, specification_dir='',
host=None, server=None, arguments=None, auth_all_paths=False, debug=False,
validator_map=None, options=None, **old_style_options):
validator_map=None, options=None):
"""
:param import_name: the name of the application package
:type import_name: str
Expand Down Expand Up @@ -47,9 +47,7 @@ def __init__(self, import_name, api_cls, port=None, specification_dir='',
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.options = ConnexionOptions(options)

self.app = self.create_app()
self.server = server
Expand Down Expand Up @@ -90,7 +88,7 @@ def set_errors_handlers(self):
def add_api(self, specification, base_path=None, arguments=None,
auth_all_paths=None, validate_responses=False,
strict_validation=False, resolver=Resolver(), resolver_error=None,
pythonic_params=False, options=None, pass_context_arg_name=None, **old_style_options):
pythonic_params=False, pass_context_arg_name=None, options=None):
"""
Adds an API to the application based on a swagger file or API dict
Expand All @@ -117,9 +115,6 @@ def add_api(self, specification, base_path=None, arguments=None,
:type options: dict | None
:param pass_context_arg_name: Name of argument in handler functions to pass request context to.
:type pass_context_arg_name: str | 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
"""
# Turn the resolver_error code into a handler object
Expand All @@ -140,12 +135,7 @@ def add_api(self, specification, base_path=None, arguments=None,
else:
specification = self.specification_dir / specification

# Old style options have higher priority compared to the already
# defined options in the App class
api_options = self.options.extend(old_style_options)

# locally defined options are added last to preserve highest priority
api_options = api_options.extend(options)
api_options = self.options.extend(options)

api = self.api_cls(specification,
base_path=base_path,
Expand All @@ -164,10 +154,6 @@ def add_api(self, specification, base_path=None, arguments=None,

def _resolver_error_handler(self, *args, **kwargs):
from connexion.handlers import ResolverErrorHandler
kwargs['operation'] = {
'operationId': 'connexion.handlers.ResolverErrorHandler',
}
kwargs.setdefault('app_consumes', ['application/json'])
return ResolverErrorHandler(self.api_cls, self.resolver_error, *args, **kwargs)

def add_url_rule(self, rule, endpoint=None, view_func=None, **options):
Expand Down

0 comments on commit 8abe554

Please sign in to comment.