From 841e999710036d2e49f39c893480aabf82d283f4 Mon Sep 17 00:00:00 2001 From: p1c2u Date: Tue, 30 Mar 2021 13:09:39 +0100 Subject: [PATCH] Media type finder --- openapi_core/contrib/falcon/handlers.py | 4 ++-- openapi_core/contrib/flask/handlers.py | 4 ++-- openapi_core/schema/content/exceptions.py | 17 ------------- openapi_core/schema/content/models.py | 18 +------------- openapi_core/schema/request_bodies/models.py | 8 ------- openapi_core/schema/responses/models.py | 11 --------- .../templating/media_types/__init__.py | 0 .../templating/media_types/exceptions.py | 19 +++++++++++++++ .../templating/media_types/finders.py | 24 +++++++++++++++++++ openapi_core/validation/request/validators.py | 7 +++--- .../validation/response/validators.py | 7 +++--- openapi_core/validation/validators.py | 5 ++++ .../contrib/falcon/test_falcon_middlewares.py | 7 +++--- .../contrib/flask/test_flask_decorator.py | 7 +++--- .../contrib/flask/test_flask_views.py | 7 +++--- tests/integration/validation/test_petstore.py | 4 ++-- .../integration/validation/test_validators.py | 8 +++---- 17 files changed, 78 insertions(+), 79 deletions(-) delete mode 100644 openapi_core/schema/content/exceptions.py create mode 100644 openapi_core/templating/media_types/__init__.py create mode 100644 openapi_core/templating/media_types/exceptions.py create mode 100644 openapi_core/templating/media_types/finders.py diff --git a/openapi_core/contrib/falcon/handlers.py b/openapi_core/contrib/falcon/handlers.py index 4f53560..85d96a6 100644 --- a/openapi_core/contrib/falcon/handlers.py +++ b/openapi_core/contrib/falcon/handlers.py @@ -5,7 +5,7 @@ from falcon.constants import MEDIA_JSON from falcon.status_codes import ( HTTP_400, HTTP_404, HTTP_405, HTTP_415, ) -from openapi_core.schema.media_types.exceptions import InvalidContentType +from openapi_core.templating.media_types.exceptions import MediaTypeNotFound from openapi_core.templating.paths.exceptions import ( ServerNotFound, OperationNotFound, PathNotFound, ) @@ -17,7 +17,7 @@ class FalconOpenAPIErrorsHandler(object): ServerNotFound: 400, OperationNotFound: 405, PathNotFound: 404, - InvalidContentType: 415, + MediaTypeNotFound: 415, } FALCON_STATUS_CODES = { diff --git a/openapi_core/contrib/flask/handlers.py b/openapi_core/contrib/flask/handlers.py index 29fecf6..393aad0 100644 --- a/openapi_core/contrib/flask/handlers.py +++ b/openapi_core/contrib/flask/handlers.py @@ -2,7 +2,7 @@ from flask.globals import current_app from flask.json import dumps -from openapi_core.schema.media_types.exceptions import InvalidContentType +from openapi_core.templating.media_types.exceptions import MediaTypeNotFound from openapi_core.templating.paths.exceptions import ( ServerNotFound, OperationNotFound, PathNotFound, ) @@ -14,7 +14,7 @@ class FlaskOpenAPIErrorsHandler(object): ServerNotFound: 400, OperationNotFound: 405, PathNotFound: 404, - InvalidContentType: 415, + MediaTypeNotFound: 415, } @classmethod diff --git a/openapi_core/schema/content/exceptions.py b/openapi_core/schema/content/exceptions.py deleted file mode 100644 index ab29b1b..0000000 --- a/openapi_core/schema/content/exceptions.py +++ /dev/null @@ -1,17 +0,0 @@ -import attr - -from openapi_core.schema.exceptions import OpenAPIMappingError - - -class OpenAPIContentError(OpenAPIMappingError): - pass - - -@attr.s(hash=True) -class MimeTypeNotFound(OpenAPIContentError): - mimetype = attr.ib() - availableMimetypes = attr.ib() - - def __str__(self): - return "Mimetype not found: {0}. Valid mimetypes: {1}".format( - self.mimetype, self.availableMimetypes) diff --git a/openapi_core/schema/content/models.py b/openapi_core/schema/content/models.py index 4af6ed8..14c066f 100644 --- a/openapi_core/schema/content/models.py +++ b/openapi_core/schema/content/models.py @@ -1,21 +1,5 @@ """OpenAPI core content models module""" -import fnmatch - -from six import iteritems - -from openapi_core.schema.content.exceptions import MimeTypeNotFound class Content(dict): - - def __getitem__(self, mimetype): - try: - return super(Content, self).__getitem__(mimetype) - except KeyError: - pass - - for key, value in iteritems(self): - if fnmatch.fnmatch(mimetype, key): - return value - - raise MimeTypeNotFound(mimetype, list(self.keys())) + pass diff --git a/openapi_core/schema/request_bodies/models.py b/openapi_core/schema/request_bodies/models.py index 42ed772..cb7b23d 100644 --- a/openapi_core/schema/request_bodies/models.py +++ b/openapi_core/schema/request_bodies/models.py @@ -1,6 +1,4 @@ """OpenAPI core request bodies models module""" -from openapi_core.schema.content.exceptions import MimeTypeNotFound -from openapi_core.schema.media_types.exceptions import InvalidContentType class RequestBody(object): @@ -11,9 +9,3 @@ class RequestBody(object): self.required = required self.extensions = extensions and dict(extensions) or {} - - def __getitem__(self, mimetype): - try: - return self.content[mimetype] - except MimeTypeNotFound: - raise InvalidContentType(mimetype) diff --git a/openapi_core/schema/responses/models.py b/openapi_core/schema/responses/models.py index c6ae437..e2e237d 100644 --- a/openapi_core/schema/responses/models.py +++ b/openapi_core/schema/responses/models.py @@ -1,6 +1,4 @@ """OpenAPI core responses models module""" -from openapi_core.schema.content.exceptions import MimeTypeNotFound -from openapi_core.schema.media_types.exceptions import InvalidContentType class Response(object): @@ -15,12 +13,3 @@ class Response(object): self.links = links and dict(links) or {} self.extensions = extensions and dict(extensions) or {} - - def __getitem__(self, mimetype): - return self.get_content_type(mimetype) - - def get_content_type(self, mimetype): - try: - return self.content[mimetype] - except MimeTypeNotFound: - raise InvalidContentType(mimetype) diff --git a/openapi_core/templating/media_types/__init__.py b/openapi_core/templating/media_types/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openapi_core/templating/media_types/exceptions.py b/openapi_core/templating/media_types/exceptions.py new file mode 100644 index 0000000..aa1240c --- /dev/null +++ b/openapi_core/templating/media_types/exceptions.py @@ -0,0 +1,19 @@ +import attr + +from openapi_core.exceptions import OpenAPIError + + +class MediaTypeFinderError(OpenAPIError): + """Media type finder error""" + + +@attr.s(hash=True) +class MediaTypeNotFound(MediaTypeFinderError): + mimetype = attr.ib() + availableMimetypes = attr.ib() + + def __str__(self): + return ( + "Content for the following mimetype not found: {0}. " + "Valid mimetypes: {1}" + ).format(self.mimetype, self.availableMimetypes) diff --git a/openapi_core/templating/media_types/finders.py b/openapi_core/templating/media_types/finders.py new file mode 100644 index 0000000..5f1c684 --- /dev/null +++ b/openapi_core/templating/media_types/finders.py @@ -0,0 +1,24 @@ +"""OpenAPI core templating media types finders module""" +import fnmatch + +from six import iteritems + +from openapi_core.templating.media_types.exceptions import MediaTypeNotFound + + +class MediaTypeFinder(object): + + def __init__(self, content): + self.content = content + + def find(self, request): + try: + return self.content[request.mimetype] + except KeyError: + pass + + for key, value in iteritems(self.content): + if fnmatch.fnmatch(request.mimetype, key): + return value + + raise MediaTypeNotFound(request.mimetype, list(self.content.keys())) diff --git a/openapi_core/validation/request/validators.py b/openapi_core/validation/request/validators.py index 4e5c4af..0f0a85e 100644 --- a/openapi_core/validation/request/validators.py +++ b/openapi_core/validation/request/validators.py @@ -4,12 +4,12 @@ from six import iteritems from openapi_core.casting.schemas.exceptions import CastError from openapi_core.deserializing.exceptions import DeserializeError -from openapi_core.schema.media_types.exceptions import InvalidContentType from openapi_core.schema.parameters.exceptions import ( MissingRequiredParameter, MissingParameter, ) from openapi_core.schema.request_bodies.exceptions import MissingRequestBody from openapi_core.security.exceptions import SecurityError +from openapi_core.templating.media_types.exceptions import MediaTypeFinderError from openapi_core.templating.paths.exceptions import PathError from openapi_core.unmarshalling.schemas.enums import UnmarshalContext from openapi_core.unmarshalling.schemas.exceptions import ( @@ -154,8 +154,9 @@ class RequestValidator(BaseValidator): return None, [] try: - media_type = operation.request_body[request.mimetype] - except InvalidContentType as exc: + media_type = self._get_media_type( + operation.request_body.content, request) + except MediaTypeFinderError as exc: return None, [exc, ] try: diff --git a/openapi_core/validation/response/validators.py b/openapi_core/validation/response/validators.py index 10acdc9..a060c01 100644 --- a/openapi_core/validation/response/validators.py +++ b/openapi_core/validation/response/validators.py @@ -1,10 +1,10 @@ """OpenAPI core validation response validators module""" from openapi_core.casting.schemas.exceptions import CastError from openapi_core.deserializing.exceptions import DeserializeError -from openapi_core.schema.media_types.exceptions import InvalidContentType from openapi_core.schema.responses.exceptions import ( InvalidResponse, MissingResponseContent, ) +from openapi_core.templating.media_types.exceptions import MediaTypeFinderError from openapi_core.templating.paths.exceptions import PathError from openapi_core.unmarshalling.schemas.enums import UnmarshalContext from openapi_core.unmarshalling.schemas.exceptions import ( @@ -70,8 +70,9 @@ class ResponseValidator(BaseValidator): return None, [] try: - media_type = operation_response[response.mimetype] - except InvalidContentType as exc: + media_type = self._get_media_type( + operation_response.content, response) + except MediaTypeFinderError as exc: return None, [exc, ] try: diff --git a/openapi_core/validation/validators.py b/openapi_core/validation/validators.py index 4d3639c..8e70d0c 100644 --- a/openapi_core/validation/validators.py +++ b/openapi_core/validation/validators.py @@ -21,6 +21,11 @@ class BaseValidator(object): finder = PathFinder(self.spec, base_url=self.base_url) return finder.find(request) + def _get_media_type(self, content, request_or_response): + from openapi_core.templating.media_types.finders import MediaTypeFinder + finder = MediaTypeFinder(content) + return finder.find(request_or_response) + def _deserialise_media_type(self, media_type, value): from openapi_core.deserializing.media_types.factories import ( MediaTypeDeserializersFactory, diff --git a/tests/integration/contrib/falcon/test_falcon_middlewares.py b/tests/integration/contrib/falcon/test_falcon_middlewares.py index fbed433..d41a738 100644 --- a/tests/integration/contrib/falcon/test_falcon_middlewares.py +++ b/tests/integration/contrib/falcon/test_falcon_middlewares.py @@ -77,12 +77,13 @@ class TestFalconOpenAPIMiddleware(object): 'errors': [ { 'class': ( - "" + "" ), 'status': 415, 'title': ( - 'Content for following mimetype not found: text/html' + "Content for the following mimetype not found: " + "text/html. Valid mimetypes: ['application/json']" ) } ] diff --git a/tests/integration/contrib/flask/test_flask_decorator.py b/tests/integration/contrib/flask/test_flask_decorator.py index afa5ad2..31c30c6 100644 --- a/tests/integration/contrib/flask/test_flask_decorator.py +++ b/tests/integration/contrib/flask/test_flask_decorator.py @@ -70,12 +70,13 @@ class TestFlaskOpenAPIDecorator(object): 'errors': [ { 'class': ( - "" + "" ), 'status': 415, 'title': ( - 'Content for following mimetype not found: text/html' + "Content for the following mimetype not found: " + "text/html. Valid mimetypes: ['application/json']" ) } ] diff --git a/tests/integration/contrib/flask/test_flask_views.py b/tests/integration/contrib/flask/test_flask_views.py index 92355e2..4134a0f 100644 --- a/tests/integration/contrib/flask/test_flask_views.py +++ b/tests/integration/contrib/flask/test_flask_views.py @@ -63,12 +63,13 @@ class TestFlaskOpenAPIView(object): 'errors': [ { 'class': ( - "" + "" ), 'status': 415, 'title': ( - 'Content for following mimetype not found: text/html' + "Content for the following mimetype not found: " + "text/html. Valid mimetypes: ['application/json']" ) } ] diff --git a/tests/integration/validation/test_petstore.py b/tests/integration/validation/test_petstore.py index 24c00b4..24c6723 100644 --- a/tests/integration/validation/test_petstore.py +++ b/tests/integration/validation/test_petstore.py @@ -12,7 +12,6 @@ from openapi_core.deserializing.parameters.exceptions import ( EmptyParameterValue, ) from openapi_core.extensions.models.models import BaseModel -from openapi_core.schema.media_types.exceptions import InvalidContentType from openapi_core.schema.parameters.exceptions import ( MissingRequiredParameter, ) @@ -20,6 +19,7 @@ from openapi_core.schema.schemas.enums import SchemaType from openapi_core.shortcuts import ( create_spec, validate_parameters, validate_body, validate_data, ) +from openapi_core.templating.media_types.exceptions import MediaTypeNotFound from openapi_core.templating.paths.exceptions import ( ServerNotFound, ) @@ -662,7 +662,7 @@ class TestPetstore(object): }, ) - with pytest.raises(InvalidContentType): + with pytest.raises(MediaTypeNotFound): validate_body(spec, request) def test_post_pets_missing_cookie(self, spec, spec_dict): diff --git a/tests/integration/validation/test_validators.py b/tests/integration/validation/test_validators.py index e244dfc..0f3b504 100644 --- a/tests/integration/validation/test_validators.py +++ b/tests/integration/validation/test_validators.py @@ -5,9 +5,6 @@ from six import text_type from openapi_core.casting.schemas.exceptions import CastError from openapi_core.deserializing.exceptions import DeserializeError -from openapi_core.schema.media_types.exceptions import ( - InvalidContentType, -) from openapi_core.extensions.models.models import BaseModel from openapi_core.schema.parameters.exceptions import MissingRequiredParameter from openapi_core.schema.request_bodies.exceptions import MissingRequestBody @@ -15,6 +12,7 @@ from openapi_core.schema.responses.exceptions import ( MissingResponseContent, InvalidResponse, ) from openapi_core.shortcuts import create_spec +from openapi_core.templating.media_types.exceptions import MediaTypeNotFound from openapi_core.templating.paths.exceptions import ( PathNotFound, OperationNotFound, ) @@ -184,7 +182,7 @@ class TestRequestValidator(object): result = validator.validate(request) assert len(result.errors) == 1 - assert type(result.errors[0]) == InvalidContentType + assert type(result.errors[0]) == MediaTypeNotFound assert result.body is None assert result.parameters == RequestParameters( header={ @@ -463,7 +461,7 @@ class TestResponseValidator(object): result = validator.validate(request, response) assert len(result.errors) == 1 - assert type(result.errors[0]) == InvalidContentType + assert type(result.errors[0]) == MediaTypeNotFound assert result.data is None assert result.headers == {}