mirror of
https://github.com/LukeHagar/connexion.git
synced 2025-12-06 04:19:26 +00:00
Update routing documentation (#1738)
Works towards #1531 Some parts of the old outing docs will need to be included on the `parameters` and `swagger-ui` pages which we still need to add.
This commit is contained in:
@@ -214,7 +214,7 @@ class AbstractApp:
|
||||
:param endpoint: the name of the endpoint for the registered URL rule, which is used for
|
||||
reverse lookup. Flask defaults to the name of the view function.
|
||||
:param view_func: the function to call when serving a request to the provided endpoint.
|
||||
:param options: the options to be forwarded to the underlying `werkzeug.routing.Rule`
|
||||
:param options: the options to be forwarded to the underlying ``werkzeug.routing.Rule``
|
||||
object. A change to Werkzeug is handling of method options. methods is a list of
|
||||
methods this rule should be limited to (`GET`, `POST` etc.). By default a rule just
|
||||
listens for `GET` (and implicitly `HEAD`).
|
||||
@@ -231,7 +231,7 @@ class AbstractApp:
|
||||
return 'Hello World'
|
||||
|
||||
:param rule: the URL rule as string
|
||||
:param options: the options to be forwarded to the underlying `werkzeug.routing.Rule`
|
||||
:param options: the options to be forwarded to the underlying ``werkzeug.routing.Rule``
|
||||
object. A change to Werkzeug is handling of method options. methods is a
|
||||
list of methods this rule should be limited to (`GET`, `POST` etc.).
|
||||
By default a rule just listens for `GET` (and implicitly `HEAD`).
|
||||
|
||||
@@ -22,27 +22,41 @@ class _RequestInterface:
|
||||
|
||||
@property
|
||||
def content_type(self) -> str:
|
||||
"""The content type included in the request headers."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def mimetype(self) -> str:
|
||||
"""The content type included in the request headers stripped from any optional character
|
||||
set encoding"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def path_params(self) -> t.Dict[str, t.Any]:
|
||||
"""Path parameters exposed as a dictionary"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def query_params(self) -> t.Dict[str, t.Any]:
|
||||
"""Query parameters exposed as a dictionary"""
|
||||
raise NotImplementedError
|
||||
|
||||
def form(self) -> t.Union[t.Dict[str, t.Any], t.Awaitable[t.Dict[str, t.Any]]]:
|
||||
"""Form data, including files."""
|
||||
raise NotImplementedError
|
||||
|
||||
def files(self) -> t.Dict[str, t.Any]:
|
||||
"""Files included in the request."""
|
||||
raise NotImplementedError
|
||||
|
||||
def json(self) -> dict:
|
||||
"""Json data included in the request."""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_body(self) -> t.Any:
|
||||
"""Get body based on the content type. This returns json data for json content types,
|
||||
form data for form content types, and bytes for all others. If the bytes data is emtpy,
|
||||
:code:`None` is returned instead."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -98,8 +112,10 @@ class WSGIRequest(_RequestInterface):
|
||||
def files(self):
|
||||
return self._werkzeug_request.files.to_dict(flat=False)
|
||||
|
||||
def json(self):
|
||||
return self.get_json(silent=True)
|
||||
|
||||
def get_body(self):
|
||||
"""Get body based on content type"""
|
||||
if self._body is None:
|
||||
if is_json_mimetype(self.content_type):
|
||||
self._body = self.get_json(silent=True)
|
||||
@@ -115,7 +131,15 @@ class WSGIRequest(_RequestInterface):
|
||||
|
||||
|
||||
class ASGIRequest(_RequestInterface):
|
||||
"""Wraps starlette Request so it can easily be extended."""
|
||||
"""
|
||||
Implementation of the Connexion :code:`_RequestInterface` representing an ASGI request.
|
||||
|
||||
.. attribute:: _starlette_request
|
||||
|
||||
This class wraps a Starlette `Request <https://www.starlette.io/requests/#request>`_,
|
||||
and provides access to its attributes by proxy.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args, uri_parser=None, **kwargs):
|
||||
self._starlette_request = StarletteRequest(*args, **kwargs)
|
||||
|
||||
@@ -114,10 +114,12 @@ class RestyResolver(Resolver):
|
||||
Resolves endpoint functions using REST semantics (unless overridden by specifying operationId)
|
||||
"""
|
||||
|
||||
def __init__(self, default_module_name, collection_endpoint_name="search"):
|
||||
def __init__(
|
||||
self, default_module_name: str, *, collection_endpoint_name: str = "search"
|
||||
):
|
||||
"""
|
||||
:param default_module_name: Default module name for operations
|
||||
:type default_module_name: str
|
||||
:param collection_endpoint_name: Name of function to resolve collection endpoints to
|
||||
"""
|
||||
super().__init__()
|
||||
self.default_module_name = default_module_name
|
||||
@@ -185,18 +187,23 @@ class RestyResolver(Resolver):
|
||||
|
||||
class MethodResolverBase(RestyResolver):
|
||||
"""
|
||||
Resolves endpoint functions based on Flask's MethodView semantics, e.g. ::
|
||||
Resolves endpoint functions based on Flask's MethodView semantics, e.g.
|
||||
|
||||
paths:
|
||||
/foo_bar:
|
||||
get:
|
||||
# Implied function call: api.FooBarView().get
|
||||
.. code-block:: yaml
|
||||
|
||||
paths:
|
||||
/foo_bar:
|
||||
get:
|
||||
# Implied function call: api.FooBarView().get
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class FooBarView(MethodView):
|
||||
def get(self):
|
||||
return ...
|
||||
def post(self):
|
||||
return ...
|
||||
|
||||
class FooBarView(MethodView):
|
||||
def get(self):
|
||||
return ...
|
||||
def post(self):
|
||||
return ...
|
||||
"""
|
||||
|
||||
_class_arguments_type = t.Dict[
|
||||
@@ -205,24 +212,22 @@ class MethodResolverBase(RestyResolver):
|
||||
|
||||
def __init__(self, *args, class_arguments: _class_arguments_type = None, **kwargs):
|
||||
"""
|
||||
:param class_arguments: Arguments to instantiate the View Class in the format # noqa
|
||||
{
|
||||
"ViewName": {
|
||||
"args": (positional arguments,)
|
||||
"kwargs": {
|
||||
"keyword": "argument"
|
||||
}
|
||||
}
|
||||
}
|
||||
:param args: Arguments passed to :class:`~RestyResolver`
|
||||
:param class_arguments: Arguments to instantiate the View Class in the format below
|
||||
:param kwargs: Keywords arguments passed to :class:`~RestyResolver`
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
{
|
||||
"ViewName": {
|
||||
"args": (positional arguments,)
|
||||
"kwargs": {
|
||||
"keyword": "argument"
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
self.class_arguments = class_arguments or {}
|
||||
if "collection_endpoint_name" in kwargs:
|
||||
del kwargs["collection_endpoint_name"]
|
||||
# Dispatch of request is done by Flask
|
||||
logger.warning(
|
||||
"collection_endpoint_name is ignored by the MethodViewResolver. "
|
||||
"Requests to a collection endpoint will be routed to .get()"
|
||||
)
|
||||
super(MethodResolverBase, self).__init__(*args, **kwargs)
|
||||
self.initialized_views: list = []
|
||||
|
||||
@@ -280,7 +285,7 @@ class MethodResolverBase(RestyResolver):
|
||||
|
||||
class MethodResolver(MethodResolverBase):
|
||||
"""
|
||||
A generic method resolver that instantiates a class a extracts the method
|
||||
A generic method resolver that instantiates a class and extracts the method
|
||||
from it, based on the operation id.
|
||||
"""
|
||||
|
||||
@@ -310,6 +315,16 @@ class MethodViewResolver(MethodResolverBase):
|
||||
It resolves the method by calling as_view on the class.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if "collection_endpoint_name" in kwargs:
|
||||
del kwargs["collection_endpoint_name"]
|
||||
# Dispatch of request is done by Flask
|
||||
logger.warning(
|
||||
"collection_endpoint_name is ignored by the MethodViewResolver. "
|
||||
"Requests to a collection endpoint will be routed to .get()"
|
||||
)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def resolve_method_from_class(self, view_name, meth_name, view_cls):
|
||||
view = None
|
||||
for v in self.initialized_views:
|
||||
|
||||
4
docs/_static/css/default.css
vendored
Normal file
4
docs/_static/css/default.css
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.rst-content .code-block-caption {
|
||||
text-align: left;
|
||||
padding: 0px, 0px, 5px, 5px;
|
||||
}
|
||||
17
docs/conf.py
17
docs/conf.py
@@ -111,6 +111,17 @@ try:
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
|
||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||
|
||||
html_theme_options = {
|
||||
'navigation_depth': 2
|
||||
}
|
||||
|
||||
html_context = {
|
||||
'display_github': True,
|
||||
'github_user': 'spec-first',
|
||||
'github_repo': 'connexion',
|
||||
'github_version': 'main/docs/',
|
||||
}
|
||||
except:
|
||||
pass
|
||||
|
||||
@@ -141,7 +152,11 @@ except:
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
# html_static_path = ['_static']
|
||||
html_static_path = ['_static']
|
||||
|
||||
html_css_files = [
|
||||
'css/default.css',
|
||||
]
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
Welcome to Connexion's documentation!
|
||||
=====================================
|
||||
|
||||
Connexion is a Python web framework that makes spec-first and api-first development easy. You
|
||||
Connexion is a modern Python web framework that makes spec-first and api-first development easy. You
|
||||
describe your API in an `OpenAPI`_ (or swagger) specification with as much detail as you want and
|
||||
Connexion will guarantee that it works as you specified.
|
||||
|
||||
|
||||
715
docs/routing.rst
715
docs/routing.rst
@@ -1,40 +1,35 @@
|
||||
Routing
|
||||
=======
|
||||
|
||||
Endpoint Routing to Your Python Views
|
||||
-------------------------------------
|
||||
Connexion leverages your OpenAPI contract to route requests to your python functions. This can
|
||||
be done in two ways:
|
||||
|
||||
Connexion uses the ``operationId`` from each `Operation Object`_ to
|
||||
identify which Python function should handle each URL.
|
||||
* `Explicitly <#explicit-routing>`_
|
||||
* `Automatically <#automatic-routing>`_
|
||||
|
||||
**Explicit Routing**:
|
||||
Explicit routing
|
||||
----------------
|
||||
|
||||
.. code-block:: yaml
|
||||
Connexion uses the :code:`operation_id` to link each `operation`_ in your API contract to
|
||||
the python function that should handle it.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: **openapi.yaml**
|
||||
|
||||
paths:
|
||||
/hello_world:
|
||||
post:
|
||||
operationId: myapp.api.hello_world
|
||||
|
||||
If you provided this path in your specification POST requests to
|
||||
``http://MYHOST/hello_world``, it would be handled by the function
|
||||
``hello_world`` in ``myapp.api`` module.
|
||||
Based on the :code:`operationId` above, any :code:`POST` request to
|
||||
:code:`http://{HOST}/hello_world`, will be handled by the :code:`hello_world` function in the
|
||||
:code:`myapp.api` module.
|
||||
|
||||
Optionally, you can include ``x-swagger-router-controller`` in your operation
|
||||
definition, making ``operationId`` relative:
|
||||
Optionally, you can include :code:`x-openapi-router-controller` or
|
||||
:code:`x-swagger-router-controller` in your :code:`operationId` to make your `operationId` relative:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
paths:
|
||||
/hello_world:
|
||||
post:
|
||||
x-swagger-router-controller: myapp.api
|
||||
operationId: hello_world
|
||||
|
||||
NOTE: If you are using an OpenAPI spec, you should use ``x-openapi-router-controller``
|
||||
in your operation definition, making ``operationId`` relative:
|
||||
|
||||
.. code-block:: yaml
|
||||
.. code-block:: python
|
||||
:caption: **openapi.yaml**
|
||||
|
||||
paths:
|
||||
/hello_world:
|
||||
@@ -42,22 +37,298 @@ in your operation definition, making ``operationId`` relative:
|
||||
x-openapi-router-controller: myapp.api
|
||||
operationId: hello_world
|
||||
|
||||
If all your operations are relative, you can use the ``RelativeResolver`` class
|
||||
instead of repeating the same ``x-swagger-router-controller`` or
|
||||
``x-openapi-router-controller`` in every operation:
|
||||
|
||||
If all your operations are relative, you can use the :code:`RelativeResolver` class when
|
||||
registering your API instead of repeating the same :code:`x-openapi-router-controller` in every
|
||||
operation:
|
||||
|
||||
.. code-block:: python
|
||||
:caption: **app.py**
|
||||
|
||||
import connexion
|
||||
from connexion.resolver import RelativeResolver
|
||||
|
||||
app = connexion.AsyncApp(__name__)
|
||||
app.add_api('openapi.yaml', resolver=RelativeResolver('myapp.api'))
|
||||
|
||||
|
||||
.. dropdown:: View a detailed reference of the :code:`RelativeResolver` class
|
||||
:icon: eye
|
||||
|
||||
.. autoclass:: connexion.resolver.RelativeResolver
|
||||
|
||||
Note that :code:`HEAD` requests will be handled by the :code:`operationId` specified under the
|
||||
:code:`GET` operation in the specification. :code:`Connexion.request.method` can be used to
|
||||
determine which request was made.
|
||||
|
||||
.. dropdown:: View a detailed reference of the :code:`connexion.request` class
|
||||
:icon: eye
|
||||
|
||||
.. autoclass:: connexion.lifecycle.ASGIRequest
|
||||
:members:
|
||||
:undoc-members:
|
||||
:inherited-members:
|
||||
|
||||
Automatic routing
|
||||
-----------------
|
||||
|
||||
Connexion can also automate the routing for you. You can choose from different :code:`Resolvers`
|
||||
implementing different resolution strategies.
|
||||
|
||||
RestyResolver
|
||||
`````````````
|
||||
|
||||
The :code:`RestyResolver` will infer an :code:`operationId` based on the path and HTTP method of
|
||||
each operation in your specification:
|
||||
|
||||
.. code-block:: python
|
||||
:caption: **app.py**
|
||||
|
||||
import connexion
|
||||
from connexion.resolver import RestyResolver
|
||||
|
||||
app = connexion.FlaskApp(__name__)
|
||||
app.add_api('swagger.yaml', resolver=RelativeResolver('api'))
|
||||
app.add_api('openapi.yaml', resolver=RestyResolver('api'))
|
||||
|
||||
.. code-block:: yaml
|
||||
:caption: **openapi.yaml**
|
||||
|
||||
paths:
|
||||
/:
|
||||
get:
|
||||
# Implied operationId: api.get
|
||||
/foo:
|
||||
get:
|
||||
# Implied operationId: api.foo.search
|
||||
post:
|
||||
# Implied operationId: api.foo.post
|
||||
/foo/{id}:
|
||||
get:
|
||||
# Implied operationId: api.foo.get
|
||||
put:
|
||||
# Implied operationId: api.foo.put
|
||||
copy:
|
||||
# Implied operationId: api.foo.copy
|
||||
delete:
|
||||
# Implied operationId: api.foo.delete
|
||||
/foo/{id}/bar:
|
||||
get:
|
||||
# Implied operationId: api.foo.bar.search
|
||||
/foo/{id}/bar/{name}:
|
||||
get:
|
||||
# Implied operationId: api.foo.bar.get
|
||||
|
||||
``RestyResolver`` will give precedence to any ``operationId`` encountered in the specification and
|
||||
respects ``x-openapi-router-controller`` and ``x-swagger-router-controller``.
|
||||
|
||||
.. dropdown:: View a detailed reference of the :code:`RestyResolver` class
|
||||
:icon: eye
|
||||
|
||||
.. autoclass:: connexion.resolver.RestyResolver
|
||||
|
||||
MethodResolver
|
||||
``````````````
|
||||
|
||||
The ``MethodResolver`` works like a ``RestyResolver``, but routes to class methods instead of
|
||||
functions.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: **app.py**
|
||||
|
||||
import connexion
|
||||
from connexion.resolver import MethodResolver
|
||||
|
||||
app = connexion.FlaskApp(__name__)
|
||||
app.add_api('openapi.yaml', resolver=MethodResolver('api'))
|
||||
|
||||
|
||||
Keep in mind that Connexion follows how `HTTP methods work in Flask`_
|
||||
and therefore HEAD requests will be handled by the ``operationId`` specified
|
||||
under GET in the specification. If both methods are supported,
|
||||
``connexion.request.method`` can be used to determine which request was made.
|
||||
.. code-block:: yaml
|
||||
:caption: **openapi.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 ``api`` module with the name
|
||||
``<<CapitalisedPath>>View``.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: **api.py**
|
||||
|
||||
class PetsView:
|
||||
|
||||
def post(self, body: dict):
|
||||
...
|
||||
|
||||
def put(self, petId, body: dict):
|
||||
...
|
||||
|
||||
def delete(self, petId):
|
||||
...
|
||||
|
||||
def get(self, petId=None):
|
||||
...
|
||||
|
||||
def search(limit=100):
|
||||
...
|
||||
|
||||
It is possible to use decorators for the Method view by listing them in the
|
||||
decorator attribute of the class:
|
||||
|
||||
.. code-block:: python
|
||||
:caption: **api.py**
|
||||
|
||||
def example_decorator(f):
|
||||
|
||||
def decorator(*args, **kwargs):
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorator
|
||||
|
||||
class PetsView:
|
||||
"""Create Pets service"""
|
||||
|
||||
decorators = [example_decorator]
|
||||
|
||||
...
|
||||
|
||||
|
||||
Additionally, you may inject dependencies into the class by declaring parameters
|
||||
for this class in the ``__init__`` method and providing the arguments in the
|
||||
``MethodViewResolver()`` call. The arguments are passed down to the class when
|
||||
``as_view`` is called.
|
||||
|
||||
A class might look like this:
|
||||
|
||||
.. code-block:: python
|
||||
:caption: **api.py**
|
||||
|
||||
class PetsView:
|
||||
def __init__(self, pets):
|
||||
self.pets = pets
|
||||
|
||||
|
||||
And the arguments are provided like this:
|
||||
|
||||
.. code-block:: python
|
||||
:caption: **app.py**
|
||||
|
||||
MethodViewResolver("api", class_arguments={"PetsView": {"kwargs": {"pets": zoo}}})
|
||||
|
||||
``MethodResolver`` will give precedence to any ``operationId`` encountered in the specification and
|
||||
respects ``x-openapi-router-controller`` and ``x-swagger-router-controller``.
|
||||
|
||||
.. dropdown:: View a detailed reference of the :code:`MethodResolver` class
|
||||
:icon: eye
|
||||
|
||||
.. autoclass:: connexion.resolver.MethodResolver
|
||||
|
||||
MethodViewResolver
|
||||
``````````````````
|
||||
|
||||
The ``MethodResolver`` works like a ``MethodViewResolver``, but routes to class methods of a
|
||||
Flask ``MethodView`` subclass.
|
||||
|
||||
.. note::
|
||||
If you migrate from connexion v2 you may want to use the ``MethodResolver`` in order to maintain
|
||||
the old behavior. The behavior described here is the new behavior, introduced in connexion v3.
|
||||
Previously, in v2, the ``MethodViewResolver`` worked like the ``MethodResolver`` in v3.
|
||||
|
||||
Another difference is that the ``MethodResolver`` will look for ``search`` and ``get``
|
||||
methods for `collection` and `single item` operations respectively, while ``MethodViewResolver``
|
||||
handles both `collection` and `single item` operations via the same ``get`` method.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: **app.py**
|
||||
|
||||
import connexion
|
||||
from connexion.resolver import MethodResolver
|
||||
|
||||
app = connexion.FlaskApp(__name__)
|
||||
app.add_api('openapi.yaml', resolver=MethodViewResolver('api'))
|
||||
|
||||
|
||||
.. code-block:: yaml
|
||||
:caption: **openapi.yaml**
|
||||
|
||||
paths:
|
||||
/foo:
|
||||
get:
|
||||
# Implied operationId: api.FooView.get
|
||||
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 ``api`` module with the name
|
||||
``<<CapitalisedPath>>View``.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: **api.py**
|
||||
|
||||
from flask.views import MethodView
|
||||
|
||||
|
||||
class PetsView(MethodView):
|
||||
|
||||
def post(self, body: dict):
|
||||
...
|
||||
|
||||
def put(self, petId, body: dict):
|
||||
...
|
||||
|
||||
def delete(self, petId):
|
||||
...
|
||||
|
||||
def get(self, petId=None, limit=100):
|
||||
...
|
||||
|
||||
.. dropdown:: View a detailed reference of the :code:`MethodViewResolver` class
|
||||
:icon: eye
|
||||
|
||||
.. autoclass:: connexion.resolver.MethodViewResolver
|
||||
|
||||
Custom resolver
|
||||
```````````````
|
||||
|
||||
You can import and extend ``connexion.resolver.Resolver`` to implement your own
|
||||
``operationId`` and function resolution algorithm.
|
||||
|
||||
.. dropdown:: View a detailed reference of the :code:`RestyResolver` class
|
||||
:icon: eye
|
||||
|
||||
.. autoclass:: connexion.resolver.Resolver
|
||||
:members:
|
||||
|
||||
.. note::
|
||||
|
||||
If you implement a custom ``Resolver``, and think it would be valuable for other users, we
|
||||
would appreciate it as a contribution.
|
||||
|
||||
|
||||
Resolver error
|
||||
--------------
|
||||
|
||||
By default, Connexion strictly enforces the presence of a handler
|
||||
function for any path defined in your specification. Because of this, adding
|
||||
@@ -68,293 +339,125 @@ added to your specification, e.g. in an API design first workflow, set the
|
||||
paths that are not yet implemented:
|
||||
|
||||
.. code-block:: python
|
||||
:caption: **app.py**
|
||||
|
||||
app = connexion.FlaskApp(__name__)
|
||||
app.add_api('swagger.yaml', resolver_error=501)
|
||||
app.add_api('openapi.yaml', resolver_error=501)
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
Automatic Routing
|
||||
-----------------
|
||||
Path parameters
|
||||
---------------
|
||||
|
||||
To customize this behavior, Connexion can use alternative
|
||||
``Resolvers``—for example, ``RestyResolver``. The ``RestyResolver``
|
||||
will compose an ``operationId`` based on the path and HTTP method of
|
||||
the endpoints in your specification:
|
||||
`Path parameters`_ are variable parts of a URL path denoted with curly braces ``{ }`` in the
|
||||
specification.
|
||||
|
||||
.. code-block:: python
|
||||
.. tab-set::
|
||||
|
||||
from connexion.resolver import RestyResolver
|
||||
.. tab-item:: OpenAPI 3
|
||||
|
||||
app = connexion.FlaskApp(__name__)
|
||||
app.add_api('swagger.yaml', resolver=RestyResolver('api'))
|
||||
.. code-block:: yaml
|
||||
|
||||
.. code-block:: yaml
|
||||
paths:
|
||||
/users/{id}:
|
||||
parameters:
|
||||
- in: path
|
||||
name: id # Note the name is the same as in the path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
description: The user ID
|
||||
|
||||
paths:
|
||||
/:
|
||||
get:
|
||||
# Implied operationId: api.get
|
||||
/foo:
|
||||
get:
|
||||
# Implied operationId: api.foo.search
|
||||
post:
|
||||
# Implied operationId: api.foo.post
|
||||
.. tab-item:: Swagger 2
|
||||
|
||||
'/foo/{id}':
|
||||
get:
|
||||
# Implied operationId: api.foo.get
|
||||
put:
|
||||
# Implied operationId: api.foo.put
|
||||
copy:
|
||||
# Implied operationId: api.foo.copy
|
||||
delete:
|
||||
# Implied operationId: api.foo.delete
|
||||
'/foo/{id}/bar':
|
||||
get:
|
||||
# Implied operationId: api.foo.bar.search
|
||||
'/foo/{id}/bar/{name}':
|
||||
get:
|
||||
# Implied operationId: api.foo.bar.get
|
||||
# Handler signature: `def get(id, name): ...`
|
||||
.. code-block:: yaml
|
||||
|
||||
``RestyResolver`` will give precedence to any ``operationId``
|
||||
encountered in the specification. It will also respect
|
||||
``x-swagger-router-controller`` and ``x-openapi-router-controller``.
|
||||
You may import and extend ``connexion.resolver.Resolver`` to implement your own
|
||||
``operationId`` (and function) resolution algorithm.
|
||||
Note that when using multiple parameters in the path, they will be
|
||||
collected and all passed to the endpoint handlers.
|
||||
paths:
|
||||
/users/{id}:
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
type: integer
|
||||
description: The user ID.
|
||||
|
||||
Automatic Routing with MethodViewResolver
|
||||
-------------------------------------------
|
||||
By default this will capture characters up to the end of the path or the next `/`.
|
||||
|
||||
.. note::
|
||||
If you migrate from connexion v2 you may want to use the `MethodResolver`
|
||||
in order to maintain the old behavior. The behavior described here is the new behavior,
|
||||
introduced in connexion v3. The difference is that the `MethodResolver` works with any
|
||||
class, while the `MethodViewResolver` is specifically designed to work with flask's
|
||||
`MethodView`. Previously, in v2, the `MethodViewResolver` worked like the `MethodResolver`
|
||||
in v3. One consequence is that the `MethodResolver` will look for `search` and `get`
|
||||
methods for list and single operations respectively, while `MethodViewResolver` uses
|
||||
the `dispatch_request` method of the given class and therefore handles both, list and
|
||||
single operations via the same `get` method.
|
||||
You can use convertors to modify what is captured. The available convertors are:
|
||||
|
||||
``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).
|
||||
* `str` returns a string, and is the default.
|
||||
* `int` returns a Python integer.
|
||||
* `float` returns a Python float.
|
||||
* `path` returns the rest of the path, including any additional `/` characters.
|
||||
|
||||
.. code-block:: python
|
||||
Convertors are used by defining them as the ``format`` in the parameter specification
|
||||
|
||||
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.views import MethodView
|
||||
|
||||
|
||||
class PetsView(MethodView):
|
||||
"""Create Pets service"""
|
||||
|
||||
pets = {}
|
||||
|
||||
def post(self, body: dict):
|
||||
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: dict):
|
||||
name = body["name"]
|
||||
tag = body.get("tag")
|
||||
pet = self.pets.get(petId, {"id": petId})
|
||||
pet["name"] = name
|
||||
pet["tag"] = tag
|
||||
pet["last_updated"] = datetime.datetime.now()
|
||||
self.pets[petId] = pet
|
||||
return self.pets[petId], 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=None, limit=100):
|
||||
if petId is None:
|
||||
# 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]
|
||||
if self.pets.get(petId) is None:
|
||||
return NoContent, 404
|
||||
return self.pets[petId]
|
||||
|
||||
|
||||
and a __init__.py file to make the Class visible in the api directory.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from .petsview import PetsView
|
||||
|
||||
|
||||
The `as_view` method of the class is called to create the view function.
|
||||
Its `dispatch_request` method is used to route requests based on the HTTP method.
|
||||
Therefore it is required to use the same `get` method for both, collection and
|
||||
single resources. I.E. `/pets` and `/pets/{id}`.
|
||||
|
||||
It is possible to use decorators for the Method view by listing them in the
|
||||
decorator attribute of the class:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def example_decorator(f):
|
||||
|
||||
def decorator(*args, **kwargs):
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorator
|
||||
|
||||
class PetsView(MethodView):
|
||||
"""Create Pets service"""
|
||||
|
||||
decorators = [example_decorator]
|
||||
|
||||
...
|
||||
|
||||
|
||||
Additionally, you may inject dependencies into the class by declaring parameters
|
||||
for this class in the `__init__` method and providing the arguments in the
|
||||
`MethodViewResolver` call. The arguments are passed down to the class when
|
||||
`as_view` is called.
|
||||
|
||||
A class might look like this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class PetsView(MethodView):
|
||||
def __init__(self, pets):
|
||||
self.pets = pets
|
||||
|
||||
|
||||
And the arguments are provided like this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
MethodViewResolver("api", class_arguments={"PetsView": {"kwargs": {"pets": zoo}}})
|
||||
|
||||
|
||||
``MethodViewResolver`` will give precedence to any ``operationId``
|
||||
encountered in the specification. It will also respect
|
||||
``x-swagger-router-controller`` and ``x-openapi-router-controller``.
|
||||
You may import and extend ``connexion.resolver.MethodViewResolver`` to implement
|
||||
your own ``operationId`` (and function) resolution algorithm.
|
||||
|
||||
Parameter Name Sanitation
|
||||
-------------------------
|
||||
|
||||
The names of query and form parameters, as well as the name of the body
|
||||
parameter are sanitized by removing characters that are not allowed in Python
|
||||
symbols. I.e. all characters that are not letters, digits or the underscore are
|
||||
removed, and finally characters are removed from the front until a letter or an
|
||||
under-score is encountered. As an example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> re.sub('^[^a-zA-Z_]+', '', re.sub('[^0-9a-zA-Z_]', '', '$top'))
|
||||
'top'
|
||||
|
||||
Without this sanitation it would e.g. be impossible to implement an
|
||||
`OData
|
||||
<http://www.odata.org>`_ API.
|
||||
|
||||
You can also convert *CamelCase* parameters to *snake_case* automatically using `pythonic_params` option:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
app = connexion.FlaskApp(__name__)
|
||||
app.add_api('api.yaml', ..., pythonic_params=True)
|
||||
|
||||
With this option enabled, Connexion firstly converts *CamelCase* names
|
||||
to *snake_case*. Secondly it looks to see if the name matches a known built-in
|
||||
and if it does it appends an underscore to the name.
|
||||
|
||||
Parameter Variable Converters
|
||||
-----------------------------
|
||||
|
||||
Connexion supports Flask's ``int``, ``float``, and ``path`` route parameter
|
||||
`variable converters
|
||||
<http://flask.pocoo.org/docs/0.12/quickstart/#variable-rules>`_.
|
||||
Specify a route parameter's type as ``integer`` or ``number`` or its type as
|
||||
``string`` and its format as ``path`` to use these converters. For example:
|
||||
``string`` and its format as ``path`` to use these converters.
|
||||
|
||||
.. code-block:: yaml
|
||||
Path parameters are passed as arguments to your python function, see :doc:`parameters`.
|
||||
|
||||
paths:
|
||||
/greeting/{name}:
|
||||
# ...
|
||||
parameters:
|
||||
- name: name
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
format: path
|
||||
Individual paths
|
||||
----------------
|
||||
|
||||
You can also add individual paths to your application which are not described in your API
|
||||
contract. This can be useful for eg. ``/healthz`` or similar endpoints.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: **api.py**
|
||||
|
||||
@app.route("/healthz")
|
||||
def healthz():
|
||||
return 200
|
||||
|
||||
# Or as alternative to the decorator
|
||||
app.add_url_rule("/healthz", "healthz", healthz)
|
||||
|
||||
.. tab-set::
|
||||
|
||||
.. tab-item:: AsyncApp
|
||||
:sync: AsyncApp
|
||||
|
||||
.. dropdown:: View a detailed reference of the ``route`` and ``add_url_rule`` methods
|
||||
:icon: eye
|
||||
|
||||
.. automethod:: connexion.AsyncApp.route
|
||||
:noindex:
|
||||
.. automethod:: connexion.AsyncApp.add_url_rule
|
||||
:noindex:
|
||||
|
||||
.. tab-item:: FlaskApp
|
||||
:sync: FlaskApp
|
||||
|
||||
.. dropdown:: View a detailed reference of the ``route`` and ``add_url_rule`` methods
|
||||
:icon: eye
|
||||
|
||||
.. automethod:: connexion.FlaskApp.route
|
||||
:noindex:
|
||||
.. automethod:: connexion.FlaskApp.add_url_rule
|
||||
:noindex:
|
||||
|
||||
.. tab-item:: ConnexionMiddleware
|
||||
:sync: ConnexionMiddleware
|
||||
|
||||
When using the ``ConnexionMiddleware`` around an ASGI or WSGI application, you can
|
||||
register individual routes on the wrapped application.
|
||||
|
||||
will create an equivalent Flask route ``/greeting/<path:name>``, allowing
|
||||
requests to include forward slashes in the ``name`` url variable.
|
||||
|
||||
API Versioning and basePath
|
||||
---------------------------
|
||||
|
||||
Setting a base path is useful for versioned APIs. An example of
|
||||
a base path would be the ``1.0`` in ``http://MYHOST/1.0/hello_world``.
|
||||
a base path would be the ``1.0`` in ``http://{HOST}/1.0/hello_world``.
|
||||
|
||||
If you are using OpenAPI 3.x.x, you set your base URL path in the
|
||||
If you are using OpenAPI 3, you set your base URL path in the
|
||||
servers block of the specification. You can either specify a full
|
||||
URL, or just a relative path.
|
||||
|
||||
.. code-block:: yaml
|
||||
:caption: **openapi.yaml**
|
||||
|
||||
servers:
|
||||
- url: https://MYHOST/1.0
|
||||
- url: https://{{HOST}}/1.0
|
||||
description: full url example
|
||||
- url: /1.0
|
||||
description: relative path example
|
||||
@@ -362,10 +465,11 @@ URL, or just a relative path.
|
||||
paths:
|
||||
...
|
||||
|
||||
If you are using OpenAPI 2.0, you can define a ``basePath`` on the top level
|
||||
of your OpenAPI 2.0 specification.
|
||||
If you are using Swagger 2.0, you can define a ``basePath`` on the top level
|
||||
of your Swagger 2.0 specification.
|
||||
|
||||
.. code-block:: yaml
|
||||
:caption: **swagger.yaml**
|
||||
|
||||
basePath: /1.0
|
||||
|
||||
@@ -376,40 +480,9 @@ If you don't want to include the base path in your specification, you
|
||||
can provide it when adding the API to your application:
|
||||
|
||||
.. code-block:: python
|
||||
:caption: **app.py**
|
||||
|
||||
app.add_api('my_api.yaml', base_path='/1.0')
|
||||
|
||||
Swagger UI path
|
||||
---------------
|
||||
|
||||
Swagger UI is available at ``/ui/`` by default.
|
||||
|
||||
You can choose another path through options:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
options = {'swagger_url': '/'}
|
||||
app = connexion.App(__name__, options=options)
|
||||
|
||||
Swagger JSON
|
||||
------------
|
||||
Connexion makes the OpenAPI/Swagger specification in JSON format
|
||||
available from ``swagger.json`` in the base path of the API.
|
||||
|
||||
You can disable the Swagger JSON at the application level:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
app = connexion.FlaskApp(__name__, specification_dir='swagger/',
|
||||
swagger_json=False)
|
||||
app.add_api('my_api.yaml')
|
||||
|
||||
You can also disable it at the API level:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
app = connexion.FlaskApp(__name__, specification_dir='swagger/')
|
||||
app.add_api('my_api.yaml', swagger_json=False)
|
||||
|
||||
.. _Operation Object: https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#operation-object
|
||||
.. _HTTP Methods work in Flask: http://flask.pocoo.org/docs/1.0/quickstart/#http-methods
|
||||
.. _operation: https://swagger.io/docs/specification/paths-and-operations/#operations
|
||||
.. _Path parameters: https://swagger.io/docs/specification/describing-parameters/#path-parameters
|
||||
|
||||
@@ -290,7 +290,7 @@ def test_resty_resolve_with_default_module_name_will_resolve_resource_root_as_co
|
||||
app_produces=["application/json"],
|
||||
app_consumes=["application/json"],
|
||||
definitions={},
|
||||
resolver=RestyResolver("fakeapi", "api_list"),
|
||||
resolver=RestyResolver("fakeapi", collection_endpoint_name="api_list"),
|
||||
)
|
||||
assert operation.operation_id == "fakeapi.hello.api_list"
|
||||
|
||||
|
||||
@@ -210,7 +210,7 @@ def test_resty_resolve_with_default_module_name_will_resolve_resource_root_as_co
|
||||
path_parameters=[],
|
||||
operation={},
|
||||
components=COMPONENTS,
|
||||
resolver=RestyResolver("fakeapi", "api_list"),
|
||||
resolver=RestyResolver("fakeapi", collection_endpoint_name="api_list"),
|
||||
)
|
||||
assert operation.operation_id == "fakeapi.hello.api_list"
|
||||
|
||||
|
||||
@@ -151,20 +151,31 @@ def test_methodview_resolve_with_default_module_name_and_x_router_controller_wil
|
||||
assert operation.operation_id == "fakeapi.PetsView.search"
|
||||
|
||||
|
||||
def test_methodview_resolve_with_default_module_name_will_resolve_resource_root_as_configured(
|
||||
method_view_resolver,
|
||||
):
|
||||
def test_method_resolve_with_default_module_name_will_resolve_resource_root_as_configured():
|
||||
operation = OpenAPIOperation(
|
||||
method="GET",
|
||||
path="/pets",
|
||||
path_parameters=[],
|
||||
operation={},
|
||||
components=COMPONENTS,
|
||||
resolver=method_view_resolver("fakeapi", "api_list"),
|
||||
resolver=MethodResolver("fakeapi", collection_endpoint_name="api_list"),
|
||||
)
|
||||
assert operation.operation_id == "fakeapi.PetsView.api_list"
|
||||
|
||||
|
||||
def test_methodview_resolve_with_default_module_name_will_resolve_resource_root_as_configured():
|
||||
operation = OpenAPIOperation(
|
||||
method="GET",
|
||||
path="/pets",
|
||||
path_parameters=[],
|
||||
operation={},
|
||||
components=COMPONENTS,
|
||||
resolver=MethodViewResolver("fakeapi", collection_endpoint_name="api_list"),
|
||||
)
|
||||
# The collection_endpoint_name is ignored
|
||||
assert operation.operation_id == "fakeapi.PetsView.search"
|
||||
|
||||
|
||||
def test_methodview_resolve_with_default_module_name_will_resolve_resource_root_post_as_post(
|
||||
method_view_resolver,
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user