Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: support openapi spec version 3 #591

Closed
wants to merge 100 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
eb374a6
Remove vendored swagger-ui, add swagger-ui-bundle as optional pip ext…
dtkav Jul 10, 2018
24f87b8
use external swagger_ui_bundle package, remove vendored files
Jul 2, 2018
3ba56ff
bump swagger-ui-bundle version
Jul 2, 2018
51f9b77
isort
Jul 2, 2018
42f33c3
resolve README.rst
Jul 4, 2018
fae0073
options: basic support for oas3
Apr 26, 2018
c8da30d
split examples into swagger2 and openapi3
Apr 26, 2018
71fd269
switch to openapi-spec-validator for multi-version validation
Apr 26, 2018
db23361
change openapi3 spec default path to /openapi.json
Apr 26, 2018
8a648c7
initial support for requestBody
Apr 26, 2018
116e15c
add oas3 restyresolver example
Apr 26, 2018
0363876
bugfix for backwards compatability with swagger2
Apr 27, 2018
6bb7f95
support component parameters, empty server block
May 1, 2018
e5fc493
add sqlalchemy example for oas3
May 4, 2018
403f3c0
handle servers leading slash edge case
May 4, 2018
b7cb2ee
don't warn when using swagger2
May 24, 2018
78966e6
add oas3 basicauth example
May 24, 2018
e41a5ae
modify aio tests to load as a module, but not conflict with the aioht…
May 24, 2018
27ac875
add .pytest_cache to gitignore
May 24, 2018
4eec9e3
depend on dtkav/openapi-spec-validator until some bugs are merged ups…
May 24, 2018
38126ca
update oas3 sqlalchemy example to expect body arg, force single threa…
May 24, 2018
48b717d
pin Julian/jsonschema and depend on dtkav/openapi-spec-validator unti…
May 24, 2018
59c9bde
progress on oas3 requestBody support
May 24, 2018
7daa37d
somehow tests pass...
May 24, 2018
90ad783
setup.py can't handle github links
May 30, 2018
e187e8d
add fork notes
May 30, 2018
26b642a
Update README.rst
dtkav May 30, 2018
f26d5ee
push broken tests for JuxhinDB
May 30, 2018
cbb00f7
some progress
May 30, 2018
6c47b4d
fixes for mimetypes, and requestBody
May 31, 2018
793f85a
update fixture mimtypes and x-body-name field
May 31, 2018
421da82
fix up some tests, and remove tests for nested null properties
May 31, 2018
5cfddfd
fix responses
May 31, 2018
4717374
tests passing! what a mess!
May 31, 2018
6061a02
fix backwards compatability with swagger2 tests (must be selected in …
May 31, 2018
5f1f7d3
flake8
Jun 5, 2018
b6ebcd4
clean up operation.py
Jun 5, 2018
f319f26
single quotes...
Jun 5, 2018
27411e2
point back to p1c2u/openapi-spec-validator now that fixes are upstreamed
Jun 6, 2018
9b34a2f
sort imports
Jun 6, 2018
fdf00a9
move aiohttp tests back to where they were
Jun 6, 2018
74d0f11
parameterize fixtures to use both swagger and openapi specs
Jun 6, 2018
252b249
refactor spec version from options.py
Jun 6, 2018
7a413fa
fix restyresolver example
Jun 7, 2018
557bb86
fix ConnexionOptions reverting to swagger2, make tests more robust
Jun 7, 2018
e6550e1
fix isort
Jun 7, 2018
eb41f44
use x-body-name in sqlalchemy example
Jun 7, 2018
046e5ac
clean up spec version code
Jun 18, 2018
3f607a2
factor out HTTP form content types
Jun 18, 2018
1a92671
simplify spec reference resolution
Jun 18, 2018
ae716ff
remove bogus comment
Jun 18, 2018
fd161a0
be more specific about exception type in get_spec_version
Jun 18, 2018
fdb9ded
fix flake
Jun 18, 2018
defe386
factor out query parsing logic
Jun 18, 2018
f026c0b
make variable names consistent (defn, schema, type) and factor out oa…
Jun 18, 2018
1446290
flake
Jun 18, 2018
dcd6a12
isort..
Jun 18, 2018
779f5e8
more get_schema refactor
Jun 18, 2018
af80806
remove some notes to self
Jun 18, 2018
30fc488
use six for urlparse import
Jun 18, 2018
f667e51
remove typographical changes, better syntax for file validation
Jun 18, 2018
ab82d9c
remote more typographic changes
Jun 18, 2018
fb569e8
clean up is_nullable
Jun 19, 2018
9442502
isort..
Jun 19, 2018
a6af4c2
isort again
Jun 19, 2018
8d3a95e
remove typographic changes
Jun 21, 2018
bdcca24
shorten key names in parameters dict comprehensions
Jun 21, 2018
9fb513c
demo class separation
Jun 19, 2018
7645933
clean up whitespace
Jun 19, 2018
e715d74
remove mention of definitions from Operation
Jun 19, 2018
40c4c69
add example response to Operation classes
Jun 19, 2018
47d2b6b
remove logging, reformatting
Jun 19, 2018
b30f0d6
split test_operation by openapi version
Jun 20, 2018
0981638
port test_resolver to Operation
Jun 20, 2018
a4b364e
isort
Jun 26, 2018
4bd2085
try operations abstract base class
Jun 26, 2018
4e3ca05
isort
Jun 27, 2018
1d5f2eb
clean up responses api
Jun 27, 2018
7a2cd0b
refactor handlers.py to rely on SecureOperation only
Jun 27, 2018
9e33255
lots of duplicate code, but everything should be separated by classes...
Jun 27, 2018
0e8388a
fix test_injection test for python2.7
Jun 29, 2018
cc699b5
centralize query and path parsing and dedup
Jun 29, 2018
69419c1
whoops, forgot the query parser file
Jun 29, 2018
6ee2174
use abc
Jun 29, 2018
e5a92bb
fix some rebase errors
Jul 4, 2018
0f830cd
remove some debug logging
Jul 4, 2018
4495a77
Refactor query and path arrays parsing logic into decorator
Jul 2, 2018
3775496
cherry pick and adapt uri_parsing decorator from swagger2 branch
Jul 4, 2018
3bacff2
remove hardcoded swagger.json from path
Jul 4, 2018
8763251
remove debug logging from decorators/response.py
Jul 4, 2018
4bef3b7
remove unused constant from decorators/validation.py
Jul 4, 2018
1e351d3
remove per-version validators
Jul 4, 2018
1edf5e6
typo
Jul 4, 2018
4a99510
fix bug with oas3 spec examples (thanks @viralanomaly)
Jul 9, 2018
3df7ef9
flake
Jul 9, 2018
e1f18d2
isort
Jul 9, 2018
e52c75e
fixes from large rebase
Jul 10, 2018
fae3fb4
remove fork notes
Jul 10, 2018
9310335
codacy lint
Jul 10, 2018
3bf0a4c
fix up codacy error in response.py
Jul 10, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ htmlcov/
*.swp
.tox/
.idea/
.pytest_cache/
26 changes: 20 additions & 6 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -397,8 +397,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 @@ -417,20 +419,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
138 changes: 87 additions & 51 deletions connexion/apis/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,17 @@
import jinja2
import six
import yaml
from swagger_spec_validator.validator20 import validate_spec
from six.moves.urllib.parse import urlparse

from ..exceptions import ResolverError
from ..operation import Operation
from ..exceptions import InvalidSpecification, ResolverError
from ..operations import OpenAPIOperation, 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 Down Expand Up @@ -68,22 +65,10 @@ def __init__(self, specification, base_path=None, arguments=None,
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)

# 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,
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 @@ -95,6 +80,22 @@ def __init__(self, specification, base_path=None, arguments=None,
self.specification = compatibility_layer(self.specification)
logger.debug('Read specification', extra={'spec': self.specification})

self.spec_version = self._get_spec_version(self.specification)

self.options = ConnexionOptions(old_style_options, oas_version=self.spec_version)
# 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('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})

# Avoid validator having ability to modify specification
spec = copy.deepcopy(self.specification)
self._validate_spec(spec)
Expand All @@ -116,6 +117,7 @@ def __init__(self, specification, base_path=None, arguments=None,
logger.debug('Security Definitions: %s', self.security_definitions)

self.definitions = self.specification.get('definitions', {})
self.components = self.specification.get('components', {})
self.parameter_definitions = self.specification.get('parameters', {})
self.response_definitions = self.specification.get('responses', {})

Expand All @@ -131,7 +133,7 @@ def __init__(self, specification, base_path=None, arguments=None,
self.pythonic_params = pythonic_params

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 @@ -142,20 +144,34 @@ 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):
if self.spec_version < (3, 0, 0):
logger.info('Using Swagger 2.0 specification')
from openapi_spec_validator import validate_v2_spec as validate_spec
else:
logger.info('Using OpenApi %d.%d.%d specification' % self.spec_version)
from openapi_spec_validator import validate_v3_spec as validate_spec
validate_spec(spec)

def _set_base_path(self, base_path):
# type: (AnyStr) -> None
if base_path is None:
self.base_path = canonical_base_path(self.specification.get('basePath', ''))
if self.spec_version >= (3, 0, 0):
# TODO variable subsitution in urls for oas3
servers = self.specification.get('servers', [])
for server in servers:
# TODO how to handle multiple servers in an oas3 spec with different paths?
self.base_path = canonical_base_path(urlparse(server['url']).path)
break
else:
self.base_path = canonical_base_path(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 @@ -187,24 +203,40 @@ 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)
if self.spec_version < (3, 0, 0):
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)
else:
operation = OpenAPIOperation(self,
method=method,
path=path,
operation=swagger_operation,
path_parameters=path_parameters,
app_security=self.security,
components=self.components,
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)

self._add_operation_internal(method, path, operation)

@abc.abstractmethod
Expand All @@ -219,18 +251,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 Expand Up @@ -287,6 +309,20 @@ def load_spec_from_file(self, arguments, specification):
swagger_string = jinja2.Template(swagger_template).render(**arguments)
return yaml.safe_load(swagger_string) # type: dict

def _get_spec_version(self, spec):
try:
version_string = spec.get('openapi') or spec.get('swagger')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try:
spec['openapi'] or spec['swagger']
except (KeyError, AttributeError):
raise ...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That has slightly different behavior. This block in my code is checking that (a) spec is a dictionary, and (b) getting the version string from either version of the spec.
Another way to do it would be to check against each version serially, but it's a bit more nested:

try:
    version_string = spec['openapi']
except (KeyError, AttributeError):
    try:
        version_string = spec["swagger"]
    except (KeyError, AttributeError):
        raise ...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OT: @dtkav thank you very much for all your work in supporting OpenAPI v3 :)

except AttributeError:
raise InvalidSpecification('Unable to get spec version')
if version_string is None:
raise InvalidSpecification('Unable to get spec version')
try:
version_tuple = tuple(map(int, version_string.split(".")))
except TypeError:
# unable to convert to semver tuple
raise InvalidSpecification('Invalid Spec Version')
return version_tuple

@classmethod
@abc.abstractmethod
def get_request(self, *args, **kwargs):
Expand Down
19 changes: 11 additions & 8 deletions connexion/apis/aiohttp_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,21 @@ 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',
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
17 changes: 11 additions & 6 deletions connexion/apis/flask_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ 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))

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
4 changes: 0 additions & 4 deletions connexion/apps/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,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
Loading