Skip to content

Commit

Permalink
Uncouple flask from App and Api. Created Abstract classes for theses …
Browse files Browse the repository at this point in the history
…modules. Fixed the tests
  • Loading branch information
dutradda committed Apr 4, 2017
1 parent 711bbf0 commit a7af62f
Show file tree
Hide file tree
Showing 30 changed files with 426 additions and 331 deletions.
36 changes: 18 additions & 18 deletions README.rst
Expand Up @@ -104,8 +104,8 @@ path of your application (e.g ``swagger/``). Then run:
import connexion
app = connexion.App(__name__, specification_dir='swagger/')
app.add_api('my_api.yaml')
app = connexion.FlaskApp(__name__, specification_dir='swagger/')
app.add_api('my_api.yaml', connexion.apis.FlaskApi)
app.run(port=8080)
See the `Connexion Pet Store Example Application`_ for a sample
Expand All @@ -132,9 +132,9 @@ Connexion uses Jinja2_ to allow specification parameterization through the `argu

.. code-block:: python
app = connexion.App(__name__, specification_dir='swagger/',
app = connexion.FlaskApp(__name__, specification_dir='swagger/',
arguments={'global': 'global_value'})
app.add_api('my_api.yaml', arguments={'api_local': 'local_value'})
app.add_api('my_api.yaml', connexion.apis.FlaskApi, arguments={'api_local': 'local_value'})
app.run(port=8080)
When a value is provided both globally and on the API, the API value will take precedence.
Expand Down Expand Up @@ -180,8 +180,8 @@ the endpoints in your specification:
from connexion.resolver import RestyResolver
app = connexion.App(__name__)
app.add_api('swagger.yaml', resolver=RestyResolver('api'))
app = connexion.FlaskApp(__name__)
app.add_api('swagger.yaml', connexion.apis.FlaskApi, resolver=RestyResolver('api'))
.. code-block:: yaml
Expand Down Expand Up @@ -283,7 +283,7 @@ to your application:

.. code-block:: python
app.add_api('my_apy.yaml', strict_validation=True)
app.add_api('my_apy.yaml', connexion.apis.FlaskApi, strict_validation=True)
API Versioning and basePath
---------------------------
Expand All @@ -306,7 +306,7 @@ can provide it when adding the API to your application:

.. code-block:: python
app.add_api('my_api.yaml', base_path='/1.0')
app.add_api('my_api.yaml', connexion.apis.FlaskApi, base_path='/1.0')
Swagger JSON
------------
Expand All @@ -317,16 +317,16 @@ You can disable the Swagger JSON at the application level:

.. code-block:: python
app = connexion.App(__name__, specification_dir='swagger/',
app = connexion.FlaskApp(__name__, specification_dir='swagger/',
swagger_json=False)
app.add_api('my_api.yaml')
app.add_api('my_api.yaml', connexion.apis.FlaskApi)
You can also disable it at the API level:

.. code-block:: python
app = connexion.App(__name__, specification_dir='swagger/')
app.add_api('my_api.yaml', swagger_json=False)
app = connexion.FlaskApp(__name__, specification_dir='swagger/')
app.add_api('my_api.yaml', connexion.apis.FlaskApi, swagger_json=False)
HTTPS Support
-------------
Expand Down Expand Up @@ -363,17 +363,17 @@ You can disable the Swagger UI at the application level:

.. code-block:: python
app = connexion.App(__name__, specification_dir='swagger/',
app = connexion.FlaskApp(__name__, specification_dir='swagger/',
swagger_ui=False)
app.add_api('my_api.yaml')
app.add_api('my_api.yaml', connexion.apis.FlaskApi)
You can also disable it at the API level:

.. code-block:: python
app = connexion.App(__name__, specification_dir='swagger/')
app.add_api('my_api.yaml', swagger_ui=False)
app = connexion.FlaskApp(__name__, specification_dir='swagger/')
app.add_api('my_api.yaml', connexion.apis.FlaskApi, swagger_ui=False)
Server Backend
--------------
Expand All @@ -386,15 +386,15 @@ this, set your server to ``tornado``:
import connexion
app = connexion.App(__name__, specification_dir='swagger/')
app = connexion.FlaskApp(__name__, specification_dir='swagger/')
app.run(server='tornado', port=8080)
You can use the Flask WSGI app with any WSGI container, e.g. `using
Flask with uWSGI`_ (this is common):

.. code-block:: python
app = connexion.App(__name__, specification_dir='swagger/')
app = connexion.FlaskApp(__name__, specification_dir='swagger/')
application = app.app # expose global WSGI application object
Set up and run the installation code:
Expand Down
4 changes: 2 additions & 2 deletions connexion/__init__.py
@@ -1,8 +1,8 @@
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
Expand Down
9 changes: 9 additions & 0 deletions connexion/apis/__init__.py
@@ -0,0 +1,9 @@

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('/')

from .abstract import AbstractApi
from .flask_api import FlaskApi
133 changes: 40 additions & 93 deletions connexion/api.py → connexion/apis/abstract.py
Expand Up @@ -3,27 +3,26 @@
import pathlib
import sys

import flask
import jinja2
import six
import werkzeug.exceptions
import abc

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
from . import canonical_base_url
from .. import utils
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 compatibility_layer(spec):
Expand All @@ -47,16 +46,10 @@ 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
Single Abstract API
"""

def __init__(self, specification, base_url=None, arguments=None,
Expand Down Expand Up @@ -111,13 +104,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 +127,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,9 +138,6 @@ 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()

if swagger_json:
self.add_swagger_json()
if swagger_ui:
Expand All @@ -160,7 +146,26 @@ 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)

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):
""""""

@abc.abstractmethod
def add_swagger_ui(self):
""""""

@abc.abstractmethod
def add_auth_on_not_found(self):
""""""

def add_operation(self, method, path, swagger_operation, path_parameters):
"""
Expand Down Expand Up @@ -197,6 +202,10 @@ 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):
""""""

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

0 comments on commit a7af62f

Please sign in to comment.