Skip to content

Commit

Permalink
Merge master into cli branch
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaelcaricio committed Sep 13, 2016
2 parents 97adb25 + 58c99a0 commit 8ef5794
Show file tree
Hide file tree
Showing 25 changed files with 336 additions and 91 deletions.
6 changes: 3 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ path of your application (e.g ``swagger/``). Then run:
See the `Connexion Pet Store Example Application`_ for a sample
specification.

Now youre able run and use Connexion!
Now you're able run and use Connexion!


OAuth 2 Authentication and Authorization
Expand Down Expand Up @@ -171,7 +171,7 @@ Automatic Routing
-----------------

To customize this behavior, Connexion can use alternative
``Resolvers``for example, ``RestyResolver``. The ``RestyResolver``
``Resolvers``--for example, ``RestyResolver``. The ``RestyResolver``
will compose an ``operationId`` based on the path and HTTP method of
the endpoints in your specification:

Expand Down Expand Up @@ -349,7 +349,7 @@ One way, `described by Flask`_, looks like this:
debug=False/True, ssl_context=context)
However, Connexion doesn't provide an ssl_context parameter. This is
because Flask doesn't, eitherbut it uses `**kwargs` to send the
because Flask doesn't, either--but it uses `**kwargs` to send the
parameters to the underlying [werkzeug](http://werkzeug.pocoo.org/) server.

The Swagger UI Console
Expand Down
79 changes: 54 additions & 25 deletions connexion/api.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,3 @@
"""
Copyright 2015 Zalando SE
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific
language governing permissions and limitations under the License.
"""

import copy
import logging
import pathlib
Expand All @@ -24,6 +11,7 @@
from swagger_spec_validator.validator20 import validate_spec

from . import utils
from .exceptions import ResolverError
from .handlers import AuthErrorHandler
from .operation import Operation
from .resolver import Resolver
Expand All @@ -32,6 +20,8 @@
SWAGGER_UI_PATH = MODULE_PATH / 'vendor' / 'swagger-ui'
SWAGGER_UI_URL = 'ui'

RESOLVER_ERROR_ENDPOINT_RANDOM_DIGITS = 6

logger = logging.getLogger('connexion.api')


Expand Down Expand Up @@ -64,7 +54,7 @@ class Api(object):
def __init__(self, swagger_yaml_path, 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):
auth_all_paths=False, debug=False, resolver_error_handler=None):
"""
:type swagger_yaml_path: pathlib.Path
:type base_url: str | None
Expand All @@ -78,8 +68,12 @@ def __init__(self, swagger_yaml_path, base_url=None, arguments=None,
:type auth_all_paths: bool
:type debug: bool
:param resolver: Callable that maps operationID to a function
:param resolver_error_handler: If given, a callable that generates an
Operation used for handling ResolveErrors
:type resolver_error_handler: callable | None
"""
self.debug = debug
self.resolver_error_handler = resolver_error_handler
self.swagger_yaml_path = pathlib.Path(swagger_yaml_path)
logger.debug('Loading specification: %s', swagger_yaml_path,
extra={'swagger_yaml': swagger_yaml_path,
Expand Down Expand Up @@ -181,6 +175,28 @@ def add_operation(self, method, path, swagger_operation, path_parameters):
validate_responses=self.validate_responses,
strict_validation=self.strict_validation,
resolver=self.resolver)
self._add_operation_internal(method, path, operation)

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)
self._add_operation_internal(method, path, operation)

def _add_operation_internal(self, method, path, operation):
operation_id = operation.operation_id
logger.debug('... Adding %s -> %s', method.upper(), operation_id,
extra=vars(operation))
Expand All @@ -202,23 +218,36 @@ def add_paths(self, paths=None):
# http://swagger.io/specification/#pathItemObject
path_parameters = methods.get('parameters', [])

# TODO Error handling
for method, endpoint in methods.items():
if method == 'parameters':
continue
try:
self.add_operation(method, path, endpoint, path_parameters)
except Exception: # pylint: disable= W0703
url = '{base_url}{path}'.format(base_url=self.base_url,
path=path)
error_msg = 'Failed to add operation for {method} {url}'.format(
method=method.upper(),
url=url)
if self.debug:
logger.exception(error_msg)
except ResolverError as err:
# If we have an error handler for resolver errors, add it
# as an operation (but randomize the flask endpoint name).
# Otherwise treat it as any other error.
if self.resolver_error_handler is not None:
self._add_resolver_error_handler(method, path, err)
else:
logger.error(error_msg)
six.reraise(*sys.exc_info())
exc_info = err.exc_info
if exc_info is None:
exc_info = sys.exc_info()
self._handle_add_operation_error(path, method, exc_info)
except Exception:
# All other relevant exceptions should be handled as well.
self._handle_add_operation_error(path, method, sys.exc_info())

def _handle_add_operation_error(self, path, method, exc_info):
url = '{base_url}{path}'.format(base_url=self.base_url, path=path)
error_msg = 'Failed to add operation for {method} {url}'.format(
method=method.upper(),
url=url)
if self.debug:
logger.exception(error_msg)
else:
logger.error(error_msg)
six.reraise(*exc_info)

def add_auth_on_not_found(self):
"""
Expand Down
32 changes: 18 additions & 14 deletions connexion/app.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,3 @@
"""
Copyright 2015 Zalando SE
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific
language governing permissions and limitations under the License.
"""

import logging
import pathlib

Expand Down Expand Up @@ -96,7 +83,7 @@ def common_error_handler(exception):

def add_api(self, swagger_file, base_path=None, arguments=None, auth_all_paths=None, swagger_json=None,
swagger_ui=None, swagger_path=None, swagger_url=None, validate_responses=False,
strict_validation=False, resolver=Resolver()):
strict_validation=False, resolver=Resolver(), resolver_error=None):
"""
Adds an API to the application based on a swagger file
Expand All @@ -122,8 +109,17 @@ def add_api(self, swagger_file, base_path=None, arguments=None, auth_all_paths=N
:type strict_validation: bool
:param resolver: Operation resolver.
:type resolver: Resolver | types.FunctionType
:param resolver_error: If specified, turns ResolverError into error
responses with the given status code.
:type resolver_error: int | None
:rtype: Api
"""
# Turn the resolver_error code into a handler object
self.resolver_error = resolver_error
resolver_error_handler = None
if resolver_error is not None:
resolver_error_handler = self._resolver_error_handler

resolver = Resolver(resolver) if hasattr(resolver, '__call__') else resolver

swagger_json = swagger_json if swagger_json is not None else self.swagger_json
Expand All @@ -143,13 +139,21 @@ def add_api(self, swagger_file, base_path=None, arguments=None, auth_all_paths=N
swagger_path=swagger_path,
swagger_url=swagger_url,
resolver=resolver,
resolver_error_handler=resolver_error_handler,
validate_responses=validate_responses,
strict_validation=strict_validation,
auth_all_paths=auth_all_paths,
debug=self.debug)
self.app.register_blueprint(api.blueprint)
return api

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

def add_error_handler(self, error_code, function):
"""
Expand Down
12 changes: 4 additions & 8 deletions connexion/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import click
from clickclick import AliasedGroup, fatal_error
from connexion import App, problem
from connexion.resolver import StubResolver

main = AliasedGroup(context_settings=dict(help_option_names=[
'-h', '--help']))
Expand Down Expand Up @@ -33,7 +32,7 @@ def validate_wsgi_server_requirements(ctx, param, value):
callback=validate_wsgi_server_requirements,
help='Which WSGI server container to use.')
@click.option('--stub',
help='Returns status code 400, and `Not Implemented Yet` payload, for '
help='Returns status code 501, and `Not Implemented Yet` payload, for '
'the endpoints which handlers are not found.',
is_flag=True, default=False)
@click.option('--hide-spec',
Expand Down Expand Up @@ -86,15 +85,12 @@ def run(spec_file,

sys.path.insert(1, path.abspath(base_path or '.'))

resolver = None
resolver_error = None
if stub:
resolver = StubResolver(lambda: problem(
title='Not Implemented Yet',
detail='The requested functionality is not implemented yet.',
status=400))
resolver_error = 501

app = App(__name__)
app.add_api(path.abspath(spec_file), resolver=resolver)
app.add_api(path.abspath(spec_file), resolver_error=resolver_error)
app.run(
port=port,
server=wsgi_server,
Expand Down
19 changes: 19 additions & 0 deletions connexion/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,25 @@ class ConnexionException(Exception):
pass


class ResolverError(LookupError):
def __init__(self, reason='Unknown reason', exc_info=None):
"""
:param reason: Reason why the resolver failed.
:type reason: str
:param exc_info: If specified, gives details of the original exception
as returned by sys.exc_info()
:type exc_info: tuple | None
"""
self.reason = reason
self.exc_info = exc_info

def __str__(self): # pragma: no cover
return '<ResolverError: {}>'.format(self.reason)

def __repr__(self): # pragma: no cover
return '<ResolverError: {}>'.format(self.reason)


class InvalidSpecification(ConnexionException):
def __init__(self, reason='Unknown Reason'):
"""
Expand Down
20 changes: 19 additions & 1 deletion connexion/handlers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

import logging

from .operation import SecureOperation
from .operation import Operation, SecureOperation
from .problem import problem

logger = logging.getLogger('connexion.handlers')
Expand Down Expand Up @@ -42,3 +42,21 @@ def handle(self, *args, **kwargs):
Actual handler for the execution after authentication.
"""
return problem(title=self.exception.name, detail=self.exception.description, status=self.exception.code)


class ResolverErrorHandler(Operation):
"""
Handler for responding to ResolverError.
"""

def __init__(self, status_code, exception, *args, **kwargs):
self.status_code = status_code
self.exception = exception
Operation.__init__(self, *args, **kwargs)

@property
def function(self):
return self.handle

def handle(self, *args, **kwargs):
return problem(title='Not Implemented', detail=self.exception.reason, status=self.status_code)
5 changes: 3 additions & 2 deletions connexion/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ class Operation(SecureOperation):
def __init__(self, method, path, operation, resolver, app_produces,
path_parameters=None, app_security=None, security_definitions=None,
definitions=None, parameter_definitions=None, response_definitions=None,
validate_responses=False, strict_validation=False):
validate_responses=False, strict_validation=False, randomize_endpoint=None):
"""
This class uses the OperationID identify the module and function that will handle the operation
Expand Down Expand Up @@ -162,6 +162,7 @@ def __init__(self, method, path, operation, resolver, app_produces,
self.validate_responses = validate_responses
self.strict_validation = strict_validation
self.operation = operation
self.randomize_endpoint = randomize_endpoint

# todo support definition references
# todo support references to application level parameters
Expand All @@ -174,7 +175,7 @@ def __init__(self, method, path, operation, resolver, app_produces,

resolution = resolver.resolve(self)
self.operation_id = resolution.operation_id
self.endpoint_name = flaskify_endpoint(self.operation_id)
self.endpoint_name = flaskify_endpoint(self.operation_id, self.randomize_endpoint)
self.__undecorated_function = resolution.function

self.validate_defaults()
Expand Down
Loading

0 comments on commit 8ef5794

Please sign in to comment.