I think that would be interesting for the library to support Flask MethodView approach. This is a good concept, inspired by the generic views from Django, and probably many people develop their APIs based on it.
Connexion should be able to detect the MethodViews endpoints specified in the swagger file.
Not supported.
This sounds a lot like Automatic Routing via RestyResolver: https://connexion.readthedocs.io/en/latest/routing.html#automatic-routing
Yes, it looks like. But the automatic routing is not able to discover my endpoints in the following format:
from flask.views import MethodView
class UserApi(MethodView):
def get(self):
pass
def post(self):
pass
@diegoguimaraes I'm not sure what your goal is? Do you expect UserApi to automatically be picked up by Connexion without any configuration?
As far as I can see Flask also needs a call to add_url_rule, so defining operationId would not be a huge burden in comparison.
Maybe you can flesh out a complete example (swagger.yaml + Python) of how you would expect the integration.
@hjacobs This is an open idea that I would like to discuss here. I started discussing about it with @rafaelcaricio on the possibility of supporting this Flask class based views, and seems achievable and a possible interesting feature.
One way could be adding the support for the automatic routing understand this, and maybe internally do what Flask does for the class based views in the add_url_rule and generate the routes for the methods implemented inside the class (http verbs).
@rafaelcaricio also suggested something like doing operationId: views.UserAPI#listing, operationId: views.UserAPI#put, since the operationId must be unique and it can't have more than one endpoint with operationId: views.UserAPI. Example:
paths:
/userapi:
post:
operationId: api.views.UserApi#post
...
get:
operationId: api.views.UserApi#get
...
Another way could be something like a new field to identify this new structure, sou could be easier to define the strategy to follow in how to assemble the routes, or the strategy that the automatic routing should follow. Something like:
paths:
/userapi:
viewClass: api.views.UserApi
post:
...
get:
...
What do you think about it? Any suggestions?
@diegoguimaraes I still do not get what the benefit of MethodView should be when using the OpenAPI spec? Your first YAML snippet looks like any other regular swagger.yaml and you can obviously just point operationId to your MethodView method.
@hjacobs just pointing the operationId to the MehtodView/View method doens't work.
@diegoguimaraes Late to the game: has any progress been made on using Connexion with MethodView?
MethodView is a common construct for Flask applications to be written against, in particular those people who are already structuring their api shape. So it is a logical next step from MethodView to move towards connexion and implement openapi. So it would be great to have the support in the tools so that people can migrate easily. At the moment this would mean rewriting flask apps written in MethodView format before they can be upgraded with connexion and openapi.
Any thoughts on how to progress ?
I think it would be great if the RestyResolver (which is great) could resolve class (static) methods, so one could build common functionality into a base class and then derive all api endpoints from the base class. E.g. if you have a view like this:
class Base():
...
@staticmethod
def get():
pass
@staticmethod
def post():
pass
...
class Pets(Base):
...
class People(Base):
...
Without the RestyResolver you can do something like this:
paths:
/pets:
get:
operationId: api.Pets.get
But it would be EVEN more convenient if the RestyResolver could automatically resolve to these class methods. Then you wouldn't need to specify the operationId on every endpoint ...
Not sure if that helps, but I just implemented my own class-based RestyResolver like this:
class ClassRestyResolver(RestyResolver):
"""
Resolves endpoint functions using REST semantics based on path-based classes
"""
def __init__(self, default_module_name, collection_endpoint_name='search'):
"""
:param default_module_name: Default module name for operations
:type default_module_name: str
"""
RestyResolver.__init__(self, default_module_name, collection_endpoint_name)
def resolve_operation_id(self, operation):
"""
Resolves the operationId using REST semantics unless explicitly configured in the spec
:type operation: connexion.operations.AbstractOperation
"""
if operation.operation_id:
return RestyResolver.resolve_operation_id(self, operation)
return self.resolve_operation_id_using_class_based_rest_semantics(operation)
def resolve_operation_id_using_class_based_rest_semantics(self, operation):
"""
Resolves the operationId using class-based REST semantics
:type operation: connexion.operations.AbstractOperation
"""
path_match = re.search(
r'^/?(?P<resource_name>([\w\-](?<!/))*)(?P<trailing_slash>/*)(?P<extended_path>.*)$',
operation.path
)
def get_controller_name():
x_router_controller = operation.router_controller
name = self.default_module_name
resource_name = path_match.group('resource_name')
if x_router_controller:
name = x_router_controller
elif resource_name:
resource_controller_name = resource_name.replace('-', '_').title()
name += '.' + resource_controller_name
return name
def get_function_name():
method = operation.method
is_collection_endpoint = \
method.lower() == 'get' \
and path_match.group('resource_name') \
and not path_match.group('extended_path')
return self.collection_endpoint_name if is_collection_endpoint else method.lower()
return '{}.{}'.format(get_controller_name(), get_function_name())
Essentially it just calls .title() on the resource_name on top of what the RestyResolver already does. Now I can have a project like this:
root/
/app
/api
__init__.py
views.py
/tests
And in views.py I have something like this:
class Pets:
pets = {}
@staticmethod
def post(body):
name = body.get("name")
tag = body.get("tag")
count = len(Pets.pets)
pet = {}
pet['id'] = count + 1
pet["tag"] = tag
pet["name"] = name
pet['last_updated'] = datetime.datetime.now()
Pets.pets[pet['id']] = pet
return pet, 201
@staticmethod
def put(body):
id_ = body["id"]
name = body["name"]
tag = body.get("tag")
id_ = int(id_)
pet = Pets.pets.get(id_, {"id": id_})
pet["name"] = name
pet["tag"] = tag
pet['last_updated'] = datetime.datetime.now()
Pets.pets[id_] = pet
return Pets.pets[id_]
@staticmethod
def delete(id_):
id_ = int(id_)
if Pets.pets.get(id_) is None:
return NoContent, 404
del Pets.pets[id_]
return NoContent, 204
@staticmethod
def get(petId):
id_ = int(petId)
if Pets.pets.get(id_) is None:
return NoContent, 404
return Pets.pets[id_]
@staticmethod
def search(limit=100):
# NOTE: we need to wrap it with list for Python 3 as dict_values is not JSON serializable
return list(Pets.pets.values())[0:limit]
So now the only thing I need to do is import the class-based views in the api/__init__.py e.g.
from app.api.views import Pets
Hey @smn-snkl
Would you be up for turning that into an example, and submitting a PR?
Alternately, it would be a bit more work, but you could add another resolver class (MethodViewResolver) or something.
I haven't used MethodView before, but I'm happy to review a PR if you make one (or anyone else is keen to).
@dtkav, sure. I'll take a look at MethodView first as I haven't used it either, but it sounds very similar to my generic class-based solution. Will submit a PR soon.
@dtkav I submitted a PR with a quick solution for this feature by modifying RestyResolver's resolve_function_from_operation_id. Pls take a look and review. I tested it briefly locally, but more extensive testing might be useful.
Awesome. Thanks! I think the next step is to add tests as you say.
We'll also want to update the docs and including an example.
I just added a new PR for @smn-snkl with tests included.
So we now have example and docs and (a very small doc).
Examples and Tests have been added. Can someone from Zalando team review this please?
@diegoguimaraes PR #847 resolves this and is released, so this can be closed.
Most helpful comment
@diegoguimaraes Late to the game: has any progress been made on using Connexion with MethodView?