Skip to content

Commit

Permalink
Added MethodViewResolver (#847)
Browse files Browse the repository at this point in the history
* Added MethodViewResolver

By subclassing RestyResolver and modifying its `resolve_function_from_operation_id` method, it is now possible to use automatic routing functionality with Flask's MethodView together with MethodViewResolver.

* Add MethodView example

* Add tests for methodview

* add documentation on how to use MethodViewResolver
  • Loading branch information
smn-snkl authored and jmcs committed Feb 4, 2019
1 parent b18d8b9 commit 9286745
Show file tree
Hide file tree
Showing 10 changed files with 624 additions and 0 deletions.
63 changes: 63 additions & 0 deletions connexion/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,66 @@ def get_function_name():
return self.collection_endpoint_name if is_collection_endpoint else method.lower()

return '{}.{}'.format(get_controller_name(), get_function_name())


class MethodViewResolver(RestyResolver):
"""
Resolves endpoint functions based on Flask's MethodView semantics, e.g. ::
paths:
/foo_bar:
get:
# Implied function call: api.FooBarView().get
class FooBarView(MethodView):
def get(self):
return ...
def post(self):
return ...
"""

def resolve_operation_id(self, operation):
"""
Resolves the operationId using REST semantics unless explicitly configured in the spec
Once resolved with REST semantics the view_name is capitalised and has 'View' added
to it so it now matches the Class names of the MethodView
:type operation: connexion.operations.AbstractOperation
"""
if operation.operation_id:
# If operation_id is defined then use the higher level API to resolve
return RestyResolver.resolve_operation_id(self, operation)

# Use RestyResolver to get operation_id for us (follow their naming conventions/structure)
operation_id = self.resolve_operation_id_using_rest_semantics(operation)
module_name, view_base, meth_name = operation_id.rsplit('.', 2)
view_name = view_base[0].upper() + view_base[1:] + 'View'

return "{}.{}.{}".format(module_name, view_name, meth_name)

def resolve_function_from_operation_id(self, operation_id):
"""
Invokes the function_resolver
:type operation_id: str
"""

try:
module_name, view_name, meth_name = operation_id.rsplit('.', 2)
if operation_id and not view_name.endswith('View'):
# If operation_id is not a view then assume it is a standard function
return self.function_resolver(operation_id)

mod = __import__(module_name, fromlist=[view_name])
view_cls = getattr(mod, view_name)
# Find the class and instantiate it
view = view_cls()
func = getattr(view, meth_name)
# Return the method function of the class
return func
except ImportError as e:
msg = 'Cannot resolve operationId "{}"! Import error was "{}"'.format(
operation_id, str(e))
raise ResolverError(msg, sys.exc_info())
except (AttributeError, ValueError) as e:
raise ResolverError(str(e), sys.exc_info())
109 changes: 109 additions & 0 deletions docs/routing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,115 @@ encountered in the specification. It will also respect
``connexion.resolver.Resolver`` to implement your own ``operationId``
(and function) resolution algorithm.

Automatic Routing with MethodViewResolver
-------------------------------------------

``MethodViewResolver`` is an customised Resolver based on ``RestyResolver``
to take advantage of MethodView structure of building Flask APIs.
The ``MethodViewResolver`` will compose an ``operationId`` based on the path and HTTP method of
the endpoints in your specification. The path will be based on the path you provide in the app.add_api and the path provided in the URL endpoint (specified in the swagger or openapi3).

.. code-block:: python
from connexion.resolver import MethodViewResolver
app = connexion.FlaskApp(__name__)
app.add_api('swagger.yaml', resolver=MethodViewResolver('api'))
And associated YAML

.. code-block:: yaml
paths:
/foo:
get:
# Implied operationId: api.FooView.search
post:
# Implied operationId: api.FooView.post
'/foo/{id}':
get:
# Implied operationId: api.FooView.get
put:
# Implied operationId: api.FooView.put
copy:
# Implied operationId: api.FooView.copy
delete:
# Implied operationId: api.FooView.delete
The structure expects a Class to exists inside the directory ``api`` that conforms to the naming ``<<Classname with Capitalised name>>View``.
In the above yaml the necessary MethodView implementation is as follows:

.. code-block:: python
import datetime
from connexion import NoContent
from flask import request
from flask.views import MethodView
class PetsView(MethodView):
""" Create Pets service
"""
method_decorators = []
pets = {}
def post(self):
body= request.json
name = body.get("name")
tag = body.get("tag")
count = len(self.pets)
pet = {}
pet['id'] = count + 1
pet["tag"] = tag
pet["name"] = name
pet['last_updated'] = datetime.datetime.now()
self.pets[pet['id']] = pet
return pet, 201
def put(self, petId):
body = request.json
name = body["name"]
tag = body.get("tag")
id_ = int(petId)
pet = self.pets.get(petId, {"id": id_})
pet["name"] = name
pet["tag"] = tag
pet['last_updated'] = datetime.datetime.now()
self.pets[id_] = pet
return self.pets[id_], 201
def delete(self, petId):
id_ = int(petId)
if self.pets.get(id_) is None:
return NoContent, 404
del self.pets[id_]
return NoContent, 204
def get(self, petId):
id_ = int(petId)
if self.pets.get(id_) is None:
return NoContent, 404
return self.pets[id_]
def search(self, limit=100):
# NOTE: we need to wrap it with list for Python 3 as dict_values is not JSON serializable
return list(self.pets.values())[0:limit]
and a __init__.py file to make the Class visible in the api directory.

.. code-block:: Python
from .petsview import PetsView
``MethodViewResolver`` will give precedence to any ``operationId``
encountered in the specification. It will also respect
``x-swagger-router-controller``. You may import and extend
``connexion.resolver.MethodViewResolver`` to implement your own ``operationId``
(and function) resolution algorithm.

Parameter Name Sanitation
-------------------------

Expand Down
11 changes: 11 additions & 0 deletions examples/openapi3/methodresolver/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
=====================
MethodViewResolver Example
=====================

Running:

.. code-block:: bash
$ ./app.py
Now open your browser and go to http://localhost:9090/v1.0/ui/ to see the Swagger UI.
1 change: 1 addition & 0 deletions examples/openapi3/methodresolver/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .petsview import PetsView
54 changes: 54 additions & 0 deletions examples/openapi3/methodresolver/api/petsview.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import datetime

from connexion import NoContent
from flask import request
from flask.views import MethodView


class PetsView(MethodView):
""" Create Pets service
"""
method_decorators = []
pets = {}

def post(self):
body= request.json
name = body.get("name")
tag = body.get("tag")
count = len(self.pets)
pet = {}
pet['id'] = count + 1
pet["tag"] = tag
pet["name"] = name
pet['last_updated'] = datetime.datetime.now()
self.pets[pet['id']] = pet
return pet, 201

def put(self, petId):
body = request.json
name = body["name"]
tag = body.get("tag")
id_ = int(petId)
pet = self.pets.get(petId, {"id": id_})
pet["name"] = name
pet["tag"] = tag
pet['last_updated'] = datetime.datetime.now()
self.pets[id_] = pet
return self.pets[id_], 201

def delete(self, petId):
id_ = int(petId)
if self.pets.get(id_) is None:
return NoContent, 404
del self.pets[id_]
return NoContent, 204

def get(self, petId):
id_ = int(petId)
if self.pets.get(id_) is None:
return NoContent, 404
return self.pets[id_]

def search(self, limit=100):
# NOTE: we need to wrap it with list for Python 3 as dict_values is not JSON serializable
return list(self.pets.values())[0:limit]
17 changes: 17 additions & 0 deletions examples/openapi3/methodresolver/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env python
import logging

import connexion
from connexion.resolver import MethodViewResolver

logging.basicConfig(level=logging.INFO)

if __name__ == '__main__':
app = connexion.FlaskApp(__name__, specification_dir='openapi/', debug=True)

options = {"swagger_ui": True}
app.add_api('pets-api.yaml',
options=options,
arguments={'title': 'MethodViewResolver Example'},
resolver=MethodViewResolver('api'), strict_validation=True, validate_responses=True )
app.run(port=9090)
Loading

0 comments on commit 9286745

Please sign in to comment.