2021-02-26 03:53:51 +00:00
|
|
|
import asyncio
|
|
|
|
import logging
|
2021-03-18 22:13:28 +00:00
|
|
|
from typing import Mapping
|
2021-02-26 03:53:51 +00:00
|
|
|
|
2021-03-05 20:23:23 +00:00
|
|
|
from openapi_core import create_spec # type: ignore
|
2021-03-17 20:13:35 +00:00
|
|
|
from openapi_core.casting.schemas.exceptions import CastError # type: ignore
|
2021-05-07 18:37:55 +00:00
|
|
|
from openapi_core.exceptions import ( # type: ignore
|
2021-03-17 20:13:35 +00:00
|
|
|
MissingRequestBody,
|
2021-05-07 18:37:55 +00:00
|
|
|
MissingRequiredParameter,
|
|
|
|
OpenAPIError,
|
2021-03-17 20:13:35 +00:00
|
|
|
)
|
2021-05-07 18:37:55 +00:00
|
|
|
from openapi_core.deserializing.exceptions import DeserializeError # type: ignore
|
|
|
|
from openapi_core.spec.paths import SpecPath # type: ignore
|
2021-05-07 18:24:45 +00:00
|
|
|
from openapi_core.templating.media_types.exceptions import ( # type: ignore
|
|
|
|
MediaTypeNotFound,
|
|
|
|
)
|
2021-02-26 03:53:51 +00:00
|
|
|
from openapi_core.templating.paths.exceptions import ( # type: ignore
|
|
|
|
OperationNotFound,
|
|
|
|
PathNotFound,
|
|
|
|
)
|
|
|
|
from openapi_core.unmarshalling.schemas.exceptions import ValidateError # type: ignore
|
|
|
|
from openapi_core.validation.exceptions import InvalidSecurity # type: ignore
|
2021-02-26 17:04:47 +00:00
|
|
|
import tornado.web # type: ignore
|
2021-02-26 03:53:51 +00:00
|
|
|
|
|
|
|
from tornado_openapi3.requests import RequestValidator
|
2021-03-18 20:37:34 +00:00
|
|
|
from tornado_openapi3.types import Deserializer, Formatter
|
2021-02-26 03:53:51 +00:00
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
class OpenAPIRequestHandler(tornado.web.RequestHandler):
|
2021-03-05 20:23:23 +00:00
|
|
|
"""Base class for HTTP request handlers.
|
|
|
|
|
2021-03-19 15:17:18 +00:00
|
|
|
A request handler extending :class:`tornado.web.RequestHandler` providing
|
2021-03-05 20:23:23 +00:00
|
|
|
OpenAPI spec validation on incoming requests and translating errors into
|
|
|
|
appropriate HTTP responses.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
2021-02-26 17:03:04 +00:00
|
|
|
@property
|
2021-03-05 20:23:23 +00:00
|
|
|
def spec_dict(self) -> dict:
|
|
|
|
"""The OpenAPI 3 specification
|
2021-02-26 17:03:04 +00:00
|
|
|
|
|
|
|
Override this in your request handlers to load or define your OpenAPI 3
|
|
|
|
spec.
|
|
|
|
|
2021-03-05 20:23:23 +00:00
|
|
|
:rtype: dict
|
|
|
|
|
2021-02-26 17:03:04 +00:00
|
|
|
"""
|
2021-02-27 04:03:33 +00:00
|
|
|
raise NotImplementedError()
|
2021-02-26 17:03:04 +00:00
|
|
|
|
2021-03-05 20:23:23 +00:00
|
|
|
@property
|
2021-05-07 18:37:55 +00:00
|
|
|
def spec(self) -> SpecPath:
|
2021-03-05 20:23:23 +00:00
|
|
|
"""The OpenAPI 3 specification.
|
|
|
|
|
|
|
|
Override this in your request handlers to customize how your OpenAPI 3
|
|
|
|
spec is loaded and validated.
|
|
|
|
|
2021-03-19 15:17:18 +00:00
|
|
|
:rtype: :class:`openapi_core.schema.specs.model.Spec`
|
2021-03-05 20:23:23 +00:00
|
|
|
|
|
|
|
"""
|
|
|
|
return create_spec(self.spec_dict, validate_spec=False)
|
|
|
|
|
2021-03-18 15:03:36 +00:00
|
|
|
@property
|
2021-03-18 22:13:28 +00:00
|
|
|
def custom_formatters(self) -> Mapping[str, Formatter]:
|
2021-03-18 15:03:36 +00:00
|
|
|
"""A dictionary mapping value formats to formatter objects.
|
|
|
|
|
2021-03-18 22:13:28 +00:00
|
|
|
If your schemas make use of format modifiers, you may specify them in
|
|
|
|
this dictionary paired with a Formatter object that provides methods to
|
|
|
|
validate values and unmarshal them into Python objects.
|
|
|
|
|
2021-03-19 15:17:18 +00:00
|
|
|
:rtype: Mapping[str, :class:`~tornado_openapi3.types.Formatter`]
|
2021-03-18 22:13:28 +00:00
|
|
|
|
2021-03-18 15:03:36 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
return dict()
|
|
|
|
|
2021-02-26 17:03:04 +00:00
|
|
|
@property
|
2021-03-18 22:13:28 +00:00
|
|
|
def custom_media_type_deserializers(self) -> Mapping[str, Deserializer]:
|
2021-02-26 17:03:04 +00:00
|
|
|
"""A dictionary mapping media types to deserializing functions.
|
|
|
|
|
|
|
|
If your endpoints make use of content types beyond ``application/json``,
|
|
|
|
you must add them to this dictionary with a deserializing method that
|
|
|
|
converts the raw body (as ``bytes`` or ``str``) to Python objects.
|
|
|
|
|
2021-03-19 15:17:18 +00:00
|
|
|
:rtype: Mapping[str, :attr:`~tornado_openapi3.types.Deserializer`]
|
2021-02-26 17:03:04 +00:00
|
|
|
"""
|
|
|
|
return dict()
|
2021-02-26 03:53:51 +00:00
|
|
|
|
|
|
|
async def prepare(self) -> None:
|
2021-02-26 17:03:04 +00:00
|
|
|
"""Called at the beginning of a request before *get/post/etc*.
|
|
|
|
|
|
|
|
Performs OpenAPI validation of the incoming request. Problems
|
|
|
|
encountered while validating the request are translated to HTTP error
|
|
|
|
codes:
|
|
|
|
|
2021-03-17 20:16:04 +00:00
|
|
|
+-----------------------------+----------+-------------------------------------+
|
|
|
|
|OpenAPI Errors |Error Code|Description |
|
|
|
|
+-----------------------------+----------+-------------------------------------+
|
|
|
|
|``PathNotFound`` |``404`` |Could not find the path for this |
|
|
|
|
| | |request in the OpenAPI specification.|
|
|
|
|
+-----------------------------+----------+-------------------------------------+
|
|
|
|
|``OperationNotFound`` |``405`` |Could not find the operation |
|
|
|
|
| | |specified for this request in the |
|
|
|
|
| | |OpenAPI specification. |
|
|
|
|
+-----------------------------+----------+-------------------------------------+
|
|
|
|
|``CastError``, |``400`` |The message body could not be decoded|
|
|
|
|
|``DeserializeError``, | |or did not validate against the |
|
|
|
|
|``MissingRequiredParameter``,| |specified schema. |
|
|
|
|
|``MissingRequestBody``, | | |
|
|
|
|
|``ValidateError`` | | |
|
|
|
|
+-----------------------------+----------+-------------------------------------+
|
|
|
|
|``InvalidSecurity`` |``401`` |Required authorization was missing |
|
|
|
|
| | |from the request. |
|
|
|
|
+-----------------------------+----------+-------------------------------------+
|
2021-05-07 18:24:45 +00:00
|
|
|
|``MediaTypeNotFound`` |``415`` |The content type of the request did |
|
2021-03-17 20:16:04 +00:00
|
|
|
| | |not match any of the types in the |
|
|
|
|
| | |OpenAPI specification. |
|
|
|
|
+-----------------------------+----------+-------------------------------------+
|
|
|
|
|Any other ``OpenAPIError`` |``500`` |An unexpected error occurred. |
|
|
|
|
+-----------------------------+----------+-------------------------------------+
|
2021-02-26 17:03:04 +00:00
|
|
|
|
|
|
|
To provide content in these error requests, you may override
|
|
|
|
:meth:`on_openapi_error`.
|
|
|
|
|
|
|
|
"""
|
2021-02-26 03:53:51 +00:00
|
|
|
maybe_coro = super().prepare()
|
|
|
|
if maybe_coro and asyncio.iscoroutine(maybe_coro): # pragma: no cover
|
|
|
|
await maybe_coro
|
|
|
|
|
|
|
|
validator = RequestValidator(
|
2021-02-27 04:03:33 +00:00
|
|
|
self.spec,
|
2021-03-18 15:03:36 +00:00
|
|
|
custom_formatters=self.custom_formatters,
|
2021-02-26 03:53:51 +00:00
|
|
|
custom_media_type_deserializers=self.custom_media_type_deserializers,
|
|
|
|
)
|
|
|
|
result = validator.validate(self.request)
|
|
|
|
try:
|
|
|
|
result.raise_for_errors()
|
|
|
|
except PathNotFound as e:
|
|
|
|
self.on_openapi_error(404, e)
|
|
|
|
except OperationNotFound as e:
|
|
|
|
self.on_openapi_error(405, e)
|
2021-03-17 20:13:35 +00:00
|
|
|
except (
|
|
|
|
CastError,
|
|
|
|
DeserializeError,
|
|
|
|
MissingRequiredParameter,
|
|
|
|
MissingRequestBody,
|
|
|
|
ValidateError,
|
|
|
|
) as e:
|
2021-02-26 03:53:51 +00:00
|
|
|
self.on_openapi_error(400, e)
|
|
|
|
except InvalidSecurity as e:
|
|
|
|
self.on_openapi_error(401, e)
|
2021-05-07 18:24:45 +00:00
|
|
|
except MediaTypeNotFound as e:
|
2021-02-26 03:53:51 +00:00
|
|
|
self.on_openapi_error(415, e)
|
|
|
|
except OpenAPIError as e:
|
|
|
|
logger.exception("Unexpected validation failure")
|
|
|
|
self.on_openapi_error(500, e)
|
|
|
|
self.validated = result
|
|
|
|
|
|
|
|
def on_openapi_error(self, status_code: int, error: OpenAPIError) -> None:
|
2021-02-26 17:03:04 +00:00
|
|
|
"""Sets an HTTP status code and finishes the request.
|
|
|
|
|
|
|
|
By default, no content is returned. To provide more informative
|
|
|
|
responses, you may override this method.
|
|
|
|
|
|
|
|
"""
|
2021-02-26 03:53:51 +00:00
|
|
|
self.set_status(status_code)
|
|
|
|
self.finish()
|