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

Frameworks passing #394

Merged
merged 15 commits into from
Apr 4, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 5 additions & 4 deletions connexion/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from flask import (abort, request, send_file, send_from_directory, # NOQA
render_template, render_template_string, url_for)
import werkzeug.exceptions as exceptions # NOQA
from .app import App # NOQA
from .api import Api # NOQA
from .apps import AbstractApp, FlaskApp # NOQA
from .apis import AbstractAPI, FlaskApi # NOQA
from .exceptions import ProblemException # NOQA
from .problem import problem # NOQA
from .decorators.produces import NoContent # NOQA
from .resolver import Resolution, Resolver, RestyResolver # NOQA

App = FlaskApp
Api = FlaskApi

# This version is replaced during release process.
__version__ = '2016.0.dev1'
4 changes: 4 additions & 0 deletions connexion/apis/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .abstract import AbstractAPI
from .flask_api import FlaskApi

__all__ = ['AbstractAPI', 'FlaskApi']
174 changes: 79 additions & 95 deletions connexion/api.py → connexion/apis/abstract.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,33 @@
import abc
import copy
import logging
import pathlib
import sys

import flask
import jinja2
import six
import werkzeug.exceptions

import yaml
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
import jinja2
from ..exceptions import ResolverError
from ..operation import Operation
from ..resolver import Resolver

MODULE_PATH = pathlib.Path(__file__).absolute().parent
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.api')
logger = logging.getLogger('connexion.apis')


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):
Expand All @@ -47,19 +51,13 @@ def compatibility_layer(spec):
return spec


def canonical_base_url(base_path):
@six.add_metaclass(abc.ABCMeta)
class AbstractAPI(object):
"""
Make given "basePath" a canonical base URL which can be prepended to paths starting with "/".
"""
return base_path.rstrip('/')


class Api(object):
"""
Single API that corresponds to a flask blueprint
Defines an abstract interface for a Swagger API
"""

def __init__(self, specification, base_url=None, arguments=None,
def __init__(self, specification, jsonifier, 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,
Expand Down Expand Up @@ -111,13 +109,12 @@ def __init__(self, specification, base_url=None, arguments=None,
spec = copy.deepcopy(self.specification)
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
# If base_url is not on provided then we try to read it from the swagger.yaml or use / by default
if base_url is None:
self.base_url = canonical_base_url(self.specification.get('basePath', ''))
else:
self.base_url = canonical_base_url(base_url)
self.specification['basePath'] = base_url
self._set_base_url(base_url)

# A list of MIME types the APIs can produce. This is global to all APIs but can be overridden on specific
# API calls.
Expand All @@ -135,9 +132,6 @@ def __init__(self, specification, base_url=None, arguments=None,
self.parameter_definitions = self.specification.get('parameters', {})
self.response_definitions = self.specification.get('responses', {})

self.swagger_path = swagger_path or SWAGGER_UI_PATH
self.swagger_url = swagger_url or SWAGGER_UI_URL

self.resolver = resolver or Resolver()

logger.debug('Validate Responses: %s', str(validate_responses))
Expand All @@ -149,8 +143,7 @@ def __init__(self, specification, base_url=None, arguments=None,
logger.debug('Pythonic params: %s', str(pythonic_params))
self.pythonic_params = pythonic_params

# Create blueprint and endpoints
self.blueprint = self.create_blueprint()
self.jsonifier = jsonifier

if swagger_json:
self.add_swagger_json()
Expand All @@ -160,7 +153,32 @@ def __init__(self, specification, base_url=None, arguments=None,
self.add_paths()

if auth_all_paths:
self.add_auth_on_not_found()
self.add_auth_on_not_found(self.security, self.security_definitions)
Copy link
Collaborator

Choose a reason for hiding this comment

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

What do you think about in the original method to add *args, **kwargs just to signal that it might have arguments in the subclasses?


def _set_base_url(self, base_url):
if base_url is None:
self.base_url = canonical_base_url(self.specification.get('basePath', ''))
else:
self.base_url = canonical_base_url(base_url)
self.specification['basePath'] = base_url

@abc.abstractmethod
def add_swagger_json(self):
"""
Adds swagger json to {base_url}/swagger.json
"""

@abc.abstractmethod
def add_swagger_ui(self):
"""
Adds swagger ui to {base_url}/ui/
"""

@abc.abstractmethod
def add_auth_on_not_found(self, security, security_definitions):
"""
Adds a 404 error handler to authenticate and only expose the 404 status if the security validation pass.
"""

def add_operation(self, method, path, swagger_operation, path_parameters):
"""
Expand All @@ -179,7 +197,8 @@ def add_operation(self, method, path, swagger_operation, path_parameters):
:type path: str
:type swagger_operation: dict
"""
operation = Operation(method=method,
operation = Operation(self,
method=method,
path=path,
path_parameters=path_parameters,
operation=swagger_operation,
Expand All @@ -197,6 +216,13 @@ def add_operation(self, method, path, swagger_operation, path_parameters):
pythonic_params=self.pythonic_params)
self._add_operation_internal(method, path, operation)

@abc.abstractmethod
def _add_operation_internal(self, method, path, operation):
"""
Adds the operation according to the user framework in use.
It will be used to register the operation on the user framework router.
"""

def _add_resolver_error_handler(self, method, path, err):
"""
Adds a handler for ResolverError for the given method and path.
Expand All @@ -216,14 +242,6 @@ def _add_resolver_error_handler(self, method, path, err):
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))

flask_path = utils.flaskify_path(path, operation.get_path_parameter_types())
self.blueprint.add_url_rule(flask_path, operation.endpoint_name, operation.function, methods=[method])

def add_paths(self, paths=None):
"""
Adds the paths defined in the specification as endpoints
Expand All @@ -244,8 +262,7 @@ def add_paths(self, paths=None):
try:
self.add_operation(method, path, endpoint, path_parameters)
except ResolverError as err:
# If we have an error handler for resolver errors, add it
# as an operation (but randomize the flask endpoint name).
# If we have an error handler for resolver errors, add it as an operation.
# Otherwise treat it as any other error.
if self.resolver_error_handler is not None:
self._add_resolver_error_handler(method, path, err)
Expand All @@ -269,59 +286,6 @@ def _handle_add_operation_error(self, path, method, exc_info):
logger.error(error_msg)
six.reraise(*exc_info)

def add_auth_on_not_found(self):
"""
Adds a 404 error handler to authenticate and only expose the 404 status if the security validation pass.
"""
logger.debug('Adding path not found authentication')
not_found_error = AuthErrorHandler(werkzeug.exceptions.NotFound(), security=self.security,
security_definitions=self.security_definitions)
endpoint_name = "{name}_not_found".format(name=self.blueprint.name)
self.blueprint.add_url_rule('/<path:invalid_path>', endpoint_name, not_found_error.function)

def add_swagger_json(self):
"""
Adds swagger json to {base_url}/swagger.json
"""
logger.debug('Adding swagger.json: %s/swagger.json', self.base_url)
endpoint_name = "{name}_swagger_json".format(name=self.blueprint.name)
self.blueprint.add_url_rule('/swagger.json',
endpoint_name,
lambda: flask.jsonify(self.specification))

def add_swagger_ui(self):
"""
Adds swagger ui to {base_url}/ui/
"""
logger.debug('Adding swagger-ui: %s/%s/', self.base_url, self.swagger_url)
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_endpoint_name, self.swagger_ui_static)
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),
index_endpoint_name, self.swagger_ui_index)

def create_blueprint(self, base_url=None):
"""
:type base_url: str | None
:rtype: flask.Blueprint
"""
base_url = base_url or self.base_url
logger.debug('Creating API blueprint: %s', base_url)
endpoint = utils.flaskify_endpoint(base_url)
blueprint = flask.Blueprint(endpoint, __name__, url_prefix=base_url,
template_folder=str(self.swagger_path))
return blueprint

def swagger_ui_index(self):
return flask.render_template('index.html', api_url=self.base_url)

def swagger_ui_static(self, filename):
"""
:type filename: str
"""
return flask.send_from_directory(str(self.swagger_path), filename)

def load_spec_from_file(self, arguments, specification):
arguments = arguments or {}

Expand All @@ -334,3 +298,23 @@ def load_spec_from_file(self, arguments, specification):

swagger_string = jinja2.Template(swagger_template).render(**arguments)
return yaml.safe_load(swagger_string) # type: dict

@classmethod
@abc.abstractmethod
def get_request(self, *args, **kwargs):
"""
This method converts the user framework request to a ConnexionRequest.
"""

@classmethod
@abc.abstractmethod
def get_response(self, response, mimetype=None, request=None):
"""
This method converts the ConnexionResponse to a user framework response.
:param response: A response to cast.
:param mimetype: The response mimetype.
:param request: The request associated with this response (the user framework request).

:type response: ConnexionResponse
:type mimetype: str
"""
Loading