mirror of
https://github.com/LukeHagar/connexion.git
synced 2025-12-06 04:19:26 +00:00
@@ -16,6 +16,7 @@ from connexion.decorators.parameter import (
|
|||||||
from connexion.decorators.response import (
|
from connexion.decorators.response import (
|
||||||
AsyncResponseDecorator,
|
AsyncResponseDecorator,
|
||||||
BaseResponseDecorator,
|
BaseResponseDecorator,
|
||||||
|
NoResponseDecorator,
|
||||||
SyncResponseDecorator,
|
SyncResponseDecorator,
|
||||||
)
|
)
|
||||||
from connexion.frameworks.abstract import Framework
|
from connexion.frameworks.abstract import Framework
|
||||||
@@ -94,10 +95,12 @@ class BaseDecorator:
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class FlaskDecorator(BaseDecorator):
|
class WSGIDecorator(BaseDecorator):
|
||||||
"""Decorator for usage with Flask. The parameter decorator works with a Flask request,
|
"""Decorator for usage with WSGI apps. The parameter decorator works with a Flask request,
|
||||||
and provides Flask datastructures to the view function. The response decorator returns
|
and provides Flask datastructures to the view function. This works for any WSGI app, since
|
||||||
a Flask response"""
|
we get the request via the connexion context provided by WSGI middleware.
|
||||||
|
|
||||||
|
This decorator does not parse responses, but passes them directly to the WSGI App."""
|
||||||
|
|
||||||
framework = FlaskFramework
|
framework = FlaskFramework
|
||||||
|
|
||||||
@@ -106,8 +109,8 @@ class FlaskDecorator(BaseDecorator):
|
|||||||
return SyncParameterDecorator
|
return SyncParameterDecorator
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _response_decorator_cls(self) -> t.Type[SyncResponseDecorator]:
|
def _response_decorator_cls(self) -> t.Type[BaseResponseDecorator]:
|
||||||
return SyncResponseDecorator
|
return NoResponseDecorator
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _sync_async_decorator(self) -> t.Callable[[t.Callable], t.Callable]:
|
def _sync_async_decorator(self) -> t.Callable[[t.Callable], t.Callable]:
|
||||||
@@ -133,6 +136,17 @@ class FlaskDecorator(BaseDecorator):
|
|||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
class FlaskDecorator(WSGIDecorator):
|
||||||
|
"""Decorator for usage with Connexion or Flask apps. The parameter decorator works with a
|
||||||
|
Flask request, and provides Flask datastructures to the view function.
|
||||||
|
|
||||||
|
The response decorator returns Flask responses."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _response_decorator_cls(self) -> t.Type[SyncResponseDecorator]:
|
||||||
|
return SyncResponseDecorator
|
||||||
|
|
||||||
|
|
||||||
class ASGIDecorator(BaseDecorator):
|
class ASGIDecorator(BaseDecorator):
|
||||||
"""Decorator for usage with ASGI apps. The parameter decorator works with a Starlette request,
|
"""Decorator for usage with ASGI apps. The parameter decorator works with a Starlette request,
|
||||||
and provides Starlette datastructures to the view function. This works for any ASGI app, since
|
and provides Starlette datastructures to the view function. This works for any ASGI app, since
|
||||||
@@ -148,10 +162,6 @@ class ASGIDecorator(BaseDecorator):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def _response_decorator_cls(self) -> t.Type[BaseResponseDecorator]:
|
def _response_decorator_cls(self) -> t.Type[BaseResponseDecorator]:
|
||||||
class NoResponseDecorator(BaseResponseDecorator):
|
|
||||||
def __call__(self, function: t.Callable) -> t.Callable:
|
|
||||||
return lambda request: function(request)
|
|
||||||
|
|
||||||
return NoResponseDecorator
|
return NoResponseDecorator
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import types
|
|||||||
import typing as t
|
import typing as t
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
from connexion import utils
|
||||||
from connexion.context import operation
|
from connexion.context import operation
|
||||||
from connexion.datastructures import NoContent
|
from connexion.datastructures import NoContent
|
||||||
from connexion.exceptions import NonConformingResponseHeaders
|
from connexion.exceptions import NonConformingResponseHeaders
|
||||||
from connexion.frameworks.abstract import Framework
|
from connexion.frameworks.abstract import Framework
|
||||||
from connexion.lifecycle import ConnexionResponse
|
from connexion.lifecycle import ConnexionResponse
|
||||||
from connexion.utils import is_json_mimetype
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -27,27 +27,27 @@ class BaseResponseDecorator:
|
|||||||
|
|
||||||
def build_framework_response(self, handler_response):
|
def build_framework_response(self, handler_response):
|
||||||
data, status_code, headers = self._unpack_handler_response(handler_response)
|
data, status_code, headers = self._unpack_handler_response(handler_response)
|
||||||
content_type = self._deduct_content_type(data, headers)
|
content_type = self._infer_content_type(data, headers)
|
||||||
if not self.framework.is_framework_response(data):
|
if not self.framework.is_framework_response(data):
|
||||||
data, status_code = self._prepare_body_and_status_code(
|
data = self._serialize_data(data, content_type=content_type)
|
||||||
data, status_code=status_code, mimetype=content_type
|
status_code = status_code or self._infer_status_code(data)
|
||||||
)
|
headers = self._update_headers(headers, content_type=content_type)
|
||||||
return self.framework.build_response(
|
return self.framework.build_response(
|
||||||
data, content_type=content_type, status_code=status_code, headers=headers
|
data, content_type=content_type, status_code=status_code, headers=headers
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _deduct_content_type(data: t.Any, headers: dict) -> str:
|
def _infer_content_type(data: t.Any, headers: dict) -> t.Optional[str]:
|
||||||
"""Deduct the response content type from the returned data, headers and operation spec.
|
"""Infer the response content type from the returned data, headers and operation spec.
|
||||||
|
|
||||||
:param data: Response data
|
:param data: Response data
|
||||||
:param headers: Headers returned by the handler.
|
:param headers: Headers returned by the handler.
|
||||||
|
|
||||||
:return: Deducted content type
|
:return: Inferred content type
|
||||||
|
|
||||||
:raises: NonConformingResponseHeaders if content type cannot be deducted.
|
:raises: NonConformingResponseHeaders if content type cannot be deducted.
|
||||||
"""
|
"""
|
||||||
content_type = headers.get("Content-Type")
|
content_type = utils.extract_content_type(headers)
|
||||||
|
|
||||||
# TODO: don't default
|
# TODO: don't default
|
||||||
produces = list(set(operation.produces))
|
produces = list(set(operation.produces))
|
||||||
@@ -66,45 +66,56 @@ class BaseResponseDecorator:
|
|||||||
pass
|
pass
|
||||||
elif len(produces) == 1:
|
elif len(produces) == 1:
|
||||||
content_type = produces[0]
|
content_type = produces[0]
|
||||||
elif isinstance(data, str) and "text/plain" in produces:
|
|
||||||
content_type = "text/plain"
|
|
||||||
elif (
|
|
||||||
isinstance(data, bytes)
|
|
||||||
or isinstance(data, (types.GeneratorType, collections.abc.Iterator))
|
|
||||||
) and "application/octet-stream" in produces:
|
|
||||||
content_type = "application/octet-stream"
|
|
||||||
else:
|
else:
|
||||||
raise NonConformingResponseHeaders(
|
if isinstance(data, str):
|
||||||
"Multiple response content types are defined in the operation spec, but the "
|
for produced_content_type in produces:
|
||||||
"handler response did not specify which one to return."
|
if "text/plain" in produced_content_type:
|
||||||
)
|
content_type = produced_content_type
|
||||||
|
elif isinstance(data, bytes) or isinstance(
|
||||||
|
data, (types.GeneratorType, collections.abc.Iterator)
|
||||||
|
):
|
||||||
|
for produced_content_type in produces:
|
||||||
|
if "application/octet-stream" in produced_content_type:
|
||||||
|
content_type = produced_content_type
|
||||||
|
|
||||||
|
if content_type is None:
|
||||||
|
raise NonConformingResponseHeaders(
|
||||||
|
"Multiple response content types are defined in the operation spec, but "
|
||||||
|
"the handler response did not specify which one to return."
|
||||||
|
)
|
||||||
|
|
||||||
return content_type
|
return content_type
|
||||||
|
|
||||||
def _prepare_body_and_status_code(
|
def _serialize_data(self, data: t.Any, *, content_type: str) -> t.Any:
|
||||||
self, data, *, status_code: int = None, mimetype: str
|
"""Serialize the data based on the content type."""
|
||||||
) -> tuple:
|
if data is None or data is NoContent:
|
||||||
if data is NoContent:
|
return None
|
||||||
data = None
|
# TODO: encode responses
|
||||||
|
mime_type, _ = utils.split_content_type(content_type)
|
||||||
if status_code is None:
|
if utils.is_json_mimetype(mime_type):
|
||||||
if data is None:
|
|
||||||
status_code = 204
|
|
||||||
else:
|
|
||||||
status_code = 200
|
|
||||||
|
|
||||||
if data is not None:
|
|
||||||
body = self._serialize_data(data, mimetype)
|
|
||||||
else:
|
|
||||||
body = data
|
|
||||||
|
|
||||||
return body, status_code
|
|
||||||
|
|
||||||
def _serialize_data(self, data: t.Any, mimetype: str) -> t.Any:
|
|
||||||
if is_json_mimetype(mimetype):
|
|
||||||
return self.jsonifier.dumps(data)
|
return self.jsonifier.dumps(data)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _infer_status_code(data: t.Any) -> int:
|
||||||
|
"""Infer the status code from the returned data."""
|
||||||
|
if data is None:
|
||||||
|
return 204
|
||||||
|
return 200
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _update_headers(
|
||||||
|
headers: t.Dict[str, str], *, content_type: str
|
||||||
|
) -> t.Dict[str, str]:
|
||||||
|
# Check if Content-Type is in headers, taking into account case-insensitivity
|
||||||
|
for key, value in headers.items():
|
||||||
|
if key.lower() == "content-type":
|
||||||
|
return headers
|
||||||
|
|
||||||
|
if content_type:
|
||||||
|
headers["Content-Type"] = content_type
|
||||||
|
return headers
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _unpack_handler_response(
|
def _unpack_handler_response(
|
||||||
handler_response: t.Union[str, bytes, dict, list, tuple]
|
handler_response: t.Union[str, bytes, dict, list, tuple]
|
||||||
@@ -186,3 +197,10 @@ class AsyncResponseDecorator(BaseResponseDecorator):
|
|||||||
return self.build_framework_response(handler_response)
|
return self.build_framework_response(handler_response)
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
class NoResponseDecorator(BaseResponseDecorator):
|
||||||
|
"""Dummy decorator to skip response serialization."""
|
||||||
|
|
||||||
|
def __call__(self, function: t.Callable) -> t.Callable:
|
||||||
|
return lambda request: function(request)
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ class RequestValidationOperation:
|
|||||||
|
|
||||||
:return: A tuple of mime type, encoding
|
:return: A tuple of mime type, encoding
|
||||||
"""
|
"""
|
||||||
mime_type, encoding = utils.extract_content_type(headers)
|
content_type = utils.extract_content_type(headers)
|
||||||
|
mime_type, encoding = utils.split_content_type(content_type)
|
||||||
if mime_type is None:
|
if mime_type is None:
|
||||||
# Content-type header is not required. Take a best guess.
|
# Content-type header is not required. Take a best guess.
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ class ResponseValidationOperation:
|
|||||||
|
|
||||||
:return: A tuple of mime type, encoding
|
:return: A tuple of mime type, encoding
|
||||||
"""
|
"""
|
||||||
mime_type, encoding = utils.extract_content_type(headers)
|
content_type = utils.extract_content_type(headers)
|
||||||
|
mime_type, encoding = utils.split_content_type(content_type)
|
||||||
if mime_type is None:
|
if mime_type is None:
|
||||||
# Content-type header is not required. Take a best guess.
|
# Content-type header is not required. Take a best guess.
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -288,31 +288,56 @@ def not_installed_error(exc, *, msg=None): # pragma: no cover
|
|||||||
|
|
||||||
|
|
||||||
def extract_content_type(
|
def extract_content_type(
|
||||||
headers: t.List[t.Tuple[bytes, bytes]]
|
headers: t.Union[t.List[t.Tuple[bytes, bytes]], t.Dict[str, str]]
|
||||||
) -> t.Tuple[t.Optional[str], t.Optional[str]]:
|
) -> t.Optional[str]:
|
||||||
"""Extract the mime type and encoding from the content type headers.
|
"""Extract the mime type and encoding from the content type headers.
|
||||||
|
|
||||||
:param headers: Headers from ASGI scope
|
:param headers: Headers from ASGI scope
|
||||||
|
|
||||||
:return: A tuple of mime type, encoding
|
:return: The content type if available in headers, otherwise None
|
||||||
"""
|
"""
|
||||||
mime_type, encoding = None, None
|
content_type: t.Optional[str] = None
|
||||||
for key, value in headers:
|
|
||||||
|
header_pairs_type = t.Collection[t.Tuple[t.Union[str, bytes], t.Union[str, bytes]]]
|
||||||
|
header_pairs: header_pairs_type = headers.items() if isinstance(headers, dict) else headers # type: ignore
|
||||||
|
for key, value in header_pairs:
|
||||||
# Headers can always be decoded using latin-1:
|
# Headers can always be decoded using latin-1:
|
||||||
# https://stackoverflow.com/a/27357138/4098821
|
# https://stackoverflow.com/a/27357138/4098821
|
||||||
decoded_key = key.decode("latin-1")
|
if isinstance(key, bytes):
|
||||||
if decoded_key.lower() == "content-type":
|
decoded_key: str = key.decode("latin-1")
|
||||||
content_type = value.decode("latin-1")
|
else:
|
||||||
if ";" in content_type:
|
decoded_key = key
|
||||||
mime_type, parameters = content_type.split(";", maxsplit=1)
|
|
||||||
|
|
||||||
prefix = "charset="
|
if decoded_key.lower() == "content-type":
|
||||||
for parameter in parameters.split(";"):
|
if isinstance(value, bytes):
|
||||||
if parameter.startswith(prefix):
|
content_type = value.decode("latin-1")
|
||||||
encoding = parameter[len(prefix) :]
|
|
||||||
else:
|
else:
|
||||||
mime_type = content_type
|
content_type = value
|
||||||
break
|
break
|
||||||
|
|
||||||
|
return content_type
|
||||||
|
|
||||||
|
|
||||||
|
def split_content_type(
|
||||||
|
content_type: t.Optional[str],
|
||||||
|
) -> t.Tuple[t.Optional[str], t.Optional[str]]:
|
||||||
|
"""Split the content type in mime_type and encoding. Other parameters are ignored."""
|
||||||
|
mime_type, encoding = None, None
|
||||||
|
|
||||||
|
if content_type is None:
|
||||||
|
return mime_type, encoding
|
||||||
|
|
||||||
|
# Check for parameters
|
||||||
|
if ";" in content_type:
|
||||||
|
mime_type, parameters = content_type.split(";", maxsplit=1)
|
||||||
|
|
||||||
|
# Find parameter describing the charset
|
||||||
|
prefix = "charset="
|
||||||
|
for parameter in parameters.split(";"):
|
||||||
|
if parameter.startswith(prefix):
|
||||||
|
encoding = parameter[len(prefix) :]
|
||||||
|
else:
|
||||||
|
mime_type = content_type
|
||||||
return mime_type, encoding
|
return mime_type, encoding
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -43,13 +43,16 @@ Automatic parameter handling
|
|||||||
To activate this behavior when using the ``ConnexionMiddleware`` wrapping a third party
|
To activate this behavior when using the ``ConnexionMiddleware`` wrapping a third party
|
||||||
application, you can leverage the following decorators provided by Connexion:
|
application, you can leverage the following decorators provided by Connexion:
|
||||||
|
|
||||||
* FlaskDecorator: provides automatic parameter injection and response serialization for
|
* ``WSGIDecorator``: provides automatic parameter injection for WSGI applications. Note
|
||||||
|
that this decorator injects Werkzeug / Flask datastructures.
|
||||||
|
|
||||||
|
* ``FlaskDecorator``: provides automatic parameter injection and response serialization for
|
||||||
Flask applications.
|
Flask applications.
|
||||||
|
|
||||||
* ASGIDecorator: provides automatic parameter injection for ASGI applications. Note that
|
* ``ASGIDecorator``: provides automatic parameter injection for ASGI applications. Note that
|
||||||
this decorator injects Starlette datastructures (such as UploadFile).
|
this decorator injects Starlette datastructures (such as UploadFile).
|
||||||
|
|
||||||
* StarletteDecorator: provides automatic parameter injection and response serialization
|
* ``StarletteDecorator``: provides automatic parameter injection and response serialization
|
||||||
for Starlette applications.
|
for Starlette applications.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
@@ -57,6 +60,7 @@ Automatic parameter handling
|
|||||||
|
|
||||||
from asgi_framework import App
|
from asgi_framework import App
|
||||||
from connexion import ConnexionMiddleware
|
from connexion import ConnexionMiddleware
|
||||||
|
from connexion.decorators import ASGIDecorator
|
||||||
|
|
||||||
@app.route("/greeting/<name>", methods=["POST"])
|
@app.route("/greeting/<name>", methods=["POST"])
|
||||||
@ASGIDecorator()
|
@ASGIDecorator()
|
||||||
|
|||||||
@@ -1,97 +1,185 @@
|
|||||||
Response Handling
|
Response Handling
|
||||||
=================
|
=================
|
||||||
|
|
||||||
|
When your application returns a response, Connexion provides the following functionality based on
|
||||||
|
your OpenAPI spec:
|
||||||
|
|
||||||
|
- It automatically translates Python errors into HTTP problem responses (see :doc:`exceptions`)
|
||||||
|
- It automatically serializes the response for certain content types
|
||||||
|
- It validates the response body and headers (see :doc:`validation`)
|
||||||
|
|
||||||
|
On this page, we zoom in on the response serialization.
|
||||||
|
|
||||||
Response Serialization
|
Response Serialization
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
|
.. tab-set::
|
||||||
|
|
||||||
|
.. tab-item:: AsyncApp
|
||||||
|
:sync: AsyncApp
|
||||||
|
|
||||||
|
|
||||||
|
When working with Connexion, you can return ordinary Python types, and connexion will serialize
|
||||||
|
them into a network response.
|
||||||
|
|
||||||
|
.. tab-item:: FlaskApp
|
||||||
|
:sync: FlaskApp
|
||||||
|
|
||||||
|
When working with Connexion, you can return ordinary Python types, and connexion will serialize
|
||||||
|
them into a network response.
|
||||||
|
|
||||||
|
.. tab-item:: ConnexionMiddleware
|
||||||
|
:sync: ConnexionMiddleware
|
||||||
|
|
||||||
|
When working with Connexion, you can return ordinary Python types, and connexion will serialize
|
||||||
|
them into a network response.
|
||||||
|
|
||||||
|
To activate this behavior when using the ``ConnexionMiddleware`` wrapping a third party
|
||||||
|
application, you can leverage the following decorators provided by Connexion:
|
||||||
|
|
||||||
|
* ``FlaskDecorator``: provides automatic parameter injection and response serialization for
|
||||||
|
Flask applications.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
:caption: **app.py**
|
||||||
|
|
||||||
|
from connexion import ConnexionMiddleware
|
||||||
|
from connexion.decorators import FlaskDecorator
|
||||||
|
from flask import Flask
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app = ConnexionMiddleware(app)
|
||||||
|
app.add_api("openapi.yaml")
|
||||||
|
|
||||||
|
@app.route("/endpoint")
|
||||||
|
@FlaskDecorator()
|
||||||
|
def endpoint(name):
|
||||||
|
...
|
||||||
|
|
||||||
|
* ``StarletteDecorator``: provides automatic parameter injection and response serialization
|
||||||
|
for Starlette applications.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
:caption: **app.py**
|
||||||
|
|
||||||
|
from connexion import ConnexionMiddleware
|
||||||
|
from connexion.decorators import StarletteDecorator
|
||||||
|
from starlette.applications import Starlette
|
||||||
|
from starlette.routing import Route
|
||||||
|
|
||||||
|
@StarletteDecorator()
|
||||||
|
def endpoint(name):
|
||||||
|
...
|
||||||
|
|
||||||
|
app = Starlette(routes=[Route('/endpoint', endpoint)])
|
||||||
|
app = ConnexionMiddleware(app)
|
||||||
|
app.add_api("openapi.yaml")
|
||||||
|
|
||||||
|
For a full example, see our `Frameworks`_ example.
|
||||||
|
|
||||||
|
The generic ``connexion.decorators.WSGIDecorator`` and
|
||||||
|
``connexion.decorators.ASGIDecorator`` unfortunately don't support response
|
||||||
|
serialization, but you can extend them to implement your own decorator for a specific
|
||||||
|
WSGI or ASGI framework respectively.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
If you implement a custom decorator, and think it would be valuable for other users, we
|
||||||
|
would appreciate it as a contribution.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
:caption: **api.py**
|
||||||
|
|
||||||
|
def endpoint():
|
||||||
|
data = "success"
|
||||||
|
status_code = 200
|
||||||
|
headers = {"Content-Type": "text/plain}
|
||||||
|
return data, status_code, headers
|
||||||
|
|
||||||
|
Data
|
||||||
|
````
|
||||||
|
|
||||||
|
If your API returns responses with the ``application/json`` content type, you can return
|
||||||
|
a simple ``dict`` or ``list`` and Connexion will serialize (``json.dumps``) the data for you.
|
||||||
|
|
||||||
|
**Customizing JSON serialization**
|
||||||
|
|
||||||
|
Connexion allows you to customize the ``Jsonifier`` used to serialize json data by subclassing the
|
||||||
|
``connexion.jsonifier.Jsonifier`` class and passing it when instantiating your app or registering
|
||||||
|
an API:
|
||||||
|
|
||||||
|
.. tab-set::
|
||||||
|
|
||||||
|
.. tab-item:: AsyncApp
|
||||||
|
:sync: AsyncApp
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
:caption: **app.py**
|
||||||
|
|
||||||
|
from connexion import AsyncApp
|
||||||
|
|
||||||
|
app = AsyncApp(__name__, jsonifier=)
|
||||||
|
app.add_api("openapi.yaml", jsonifier=c)
|
||||||
|
|
||||||
|
|
||||||
|
.. tab-item:: FlaskApp
|
||||||
|
:sync: FlaskApp
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
:caption: **app.py**
|
||||||
|
|
||||||
|
from connexion import FlaskApp
|
||||||
|
|
||||||
|
app = FlaskApp(__name__, jsonifier=...)
|
||||||
|
app.add_api("openapi.yaml", jsonifier=...):
|
||||||
|
|
||||||
|
.. tab-item:: ConnexionMiddleware
|
||||||
|
:sync: ConnexionMiddleware
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
:caption: **app.py**
|
||||||
|
|
||||||
|
from asgi_framework import App
|
||||||
|
from connexion import ConnexionMiddleware
|
||||||
|
|
||||||
|
app = App(__name__)
|
||||||
|
app = ConnexionMiddleware(app, jsonifier=...)
|
||||||
|
app.add_api("openapi.yaml", jsonifier=...)
|
||||||
|
|
||||||
|
Status code
|
||||||
|
```````````
|
||||||
|
|
||||||
|
If no status code is provided, Connexion will automatically set it as ``200`` if data is
|
||||||
|
returned, or as ``204`` if ``None`` or ``connexion.datastructures.NoContent`` is returned.
|
||||||
|
|
||||||
|
Headers
|
||||||
|
```````
|
||||||
|
|
||||||
|
The headers can be used to define any response headers to return. If your OpenAPI specification
|
||||||
|
defines multiple responses with different content types, you can explicitly set the
|
||||||
|
``Content-Type`` header to tell Connexion which response to validate against.
|
||||||
|
|
||||||
|
If you do not explicitly return a ``Content-Type`` header, Connexion's behavior depends on the
|
||||||
|
Responses defined in your OpenAPI spec:
|
||||||
|
|
||||||
|
* If you have defined a single response content type in your OpenAPI specification, Connexion
|
||||||
|
will automatically set it.
|
||||||
|
* If you have defined multiple response content types in your OpenAPI specification, Connexion
|
||||||
|
will try to infer which one matches your response and set it. If it cannot infer the content
|
||||||
|
type, an error is raised.
|
||||||
|
* If you have not defined a response content type in your OpenAPI specification, Connexion will
|
||||||
|
automatically set it to ``application/json`` unless you don't return any data. This is mostly
|
||||||
|
because of backward-compatibility, and can be circumvented easily by defining a response
|
||||||
|
content type in your OpenAPI specification.
|
||||||
|
|
||||||
|
Skipping response serialization
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
If your endpoint returns an instance of ``connexion.lifecycle.ConnexionResponse``, or a
|
||||||
|
framework-specific response (``flask.Response`` or ``starlette.responses.Response``), response
|
||||||
|
serialization is skipped, and the response is passed directly to the underlying framework.
|
||||||
|
|
||||||
|
If your endpoint returns a `Response`
|
||||||
If the endpoint returns a `Response` object this response will be used as is.
|
If the endpoint returns a `Response` object this response will be used as is.
|
||||||
|
|
||||||
Otherwise, and by default and if the specification defines that an endpoint
|
.. _Frameworks: https://github.com/spec-first/connexion/tree/main/examples/frameworks
|
||||||
produces only JSON, connexion will automatically serialize the return value
|
|
||||||
for you and set the right content type in the HTTP header.
|
|
||||||
|
|
||||||
If the endpoint produces a single non-JSON mimetype then Connexion will
|
|
||||||
automatically set the right content type in the HTTP header.
|
|
||||||
|
|
||||||
Customizing JSON encoder
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
Connexion allows you to customize the `JSONEncoder` class in the Flask app
|
|
||||||
instance `json_encoder` (`connexion.App:app`). If you wanna reuse the
|
|
||||||
Connexion's date-time serialization, inherit your custom encoder from
|
|
||||||
`connexion.apps.flask_app.FlaskJSONEncoder`.
|
|
||||||
|
|
||||||
For more information on the `JSONEncoder`, see the `Flask documentation`_.
|
|
||||||
|
|
||||||
.. _Flask Documentation: https://flask.palletsprojects.com/en/2.0.x/api/#flask.json.JSONEncoder
|
|
||||||
|
|
||||||
Returning status codes
|
|
||||||
----------------------
|
|
||||||
There are two ways of returning a specific status code.
|
|
||||||
|
|
||||||
One way is to return a `Response` object that will be used unchanged.
|
|
||||||
|
|
||||||
The other is returning it as a second return value in the response. For example
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
def my_endpoint():
|
|
||||||
return 'Not Found', 404
|
|
||||||
|
|
||||||
Returning Headers
|
|
||||||
-----------------
|
|
||||||
There are two ways to return headers from your endpoints.
|
|
||||||
|
|
||||||
One way is to return a `Response` object that will be used unchanged.
|
|
||||||
|
|
||||||
The other is returning a dict with the header values as the third return value
|
|
||||||
in the response:
|
|
||||||
|
|
||||||
For example
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
def my_endpoint():
|
|
||||||
return 'Not Found', 404, {'x-error': 'not found'}
|
|
||||||
|
|
||||||
|
|
||||||
Response Validation
|
|
||||||
-------------------
|
|
||||||
While, by default Connexion doesn't validate the responses it's possible to
|
|
||||||
do so by opting in when adding the API:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import connexion
|
|
||||||
|
|
||||||
app = connexion.FlaskApp(__name__, specification_dir='swagger/')
|
|
||||||
app.add_api('my_api.yaml', validate_responses=True)
|
|
||||||
app.run(port=8080)
|
|
||||||
|
|
||||||
This will validate all the responses using `jsonschema` and is specially useful
|
|
||||||
during development.
|
|
||||||
|
|
||||||
|
|
||||||
Custom Validator
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
By default, response body contents are validated against OpenAPI schema
|
|
||||||
via ``connexion.decorators.response.ResponseValidator``, if you want to change
|
|
||||||
the validation, you can override the default class with:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
validator_map = {
|
|
||||||
'response': CustomResponseValidator
|
|
||||||
}
|
|
||||||
app = connexion.FlaskApp(__name__)
|
|
||||||
app.add_api('api.yaml', ..., validator_map=validator_map)
|
|
||||||
|
|
||||||
|
|
||||||
Error Handling
|
|
||||||
--------------
|
|
||||||
By default connexion error messages are JSON serialized according to
|
|
||||||
`Problem Details for HTTP APIs`_
|
|
||||||
|
|
||||||
Application can return errors using ``connexion.problem``.
|
|
||||||
|
|
||||||
.. _Problem Details for HTTP APIs: https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ def test_produce_decorator(simple_app):
|
|||||||
app_client = simple_app.test_client()
|
app_client = simple_app.test_client()
|
||||||
|
|
||||||
get_bye = app_client.get("/v1.0/bye/jsantos")
|
get_bye = app_client.get("/v1.0/bye/jsantos")
|
||||||
assert get_bye.headers.get("content-type") == "text/plain; charset=utf-8"
|
assert get_bye.headers.get("content-type", "").startswith("text/plain")
|
||||||
|
|
||||||
|
|
||||||
def test_returning_response_tuple(simple_app):
|
def test_returning_response_tuple(simple_app):
|
||||||
|
|||||||
Reference in New Issue
Block a user