diff --git a/docs/integrations.rst b/docs/integrations.rst index d4793f8b..fbbcaf1c 100644 --- a/docs/integrations.rst +++ b/docs/integrations.rst @@ -39,6 +39,7 @@ Falcon ------ This section describes integration with `Falcon `__ web framework. +The integration supports Falcon from version 3.0 and above. Middleware ~~~~~~~~~~ @@ -50,7 +51,7 @@ Falcon API can be integrated by `FalconOpenAPIMiddleware` middleware. from openapi_core.contrib.falcon.middlewares import FalconOpenAPIMiddleware openapi_middleware = FalconOpenAPIMiddleware.from_spec(spec) - api = falcon.API(middleware=[openapi_middleware]) + app = falcon.App(middleware=[openapi_middleware]) Low level ~~~~~~~~~ @@ -62,7 +63,7 @@ For Falcon you can use FalconOpenAPIRequest a Falcon request factory: from openapi_core.validation.request.validators import RequestValidator from openapi_core.contrib.falcon import FalconOpenAPIRequestFactory - openapi_request = FalconOpenAPIRequestFactory.create(falcon_request) + openapi_request = FalconOpenAPIRequestFactory().create(falcon_request) validator = RequestValidator(spec) result = validator.validate(openapi_request) @@ -73,7 +74,7 @@ You can use FalconOpenAPIResponse as a Falcon response factory: from openapi_core.validation.response.validators import ResponseValidator from openapi_core.contrib.falcon import FalconOpenAPIResponseFactory - openapi_response = FalconOpenAPIResponseFactory.create(falcon_response) + openapi_response = FalconOpenAPIResponseFactory().create(falcon_response) validator = ResponseValidator(spec) result = validator.validate(openapi_request, openapi_response) diff --git a/openapi_core/contrib/falcon/compat.py b/openapi_core/contrib/falcon/compat.py deleted file mode 100644 index 4e60e86c..00000000 --- a/openapi_core/contrib/falcon/compat.py +++ /dev/null @@ -1,24 +0,0 @@ -"""OpenAPI core contrib falcon compat module""" -try: - from falcon import App # noqa: F401 - HAS_FALCON3 = True -except ImportError: - HAS_FALCON3 = False - - -def get_request_media(req, default=None): - # in falcon 3 media is deprecated - return req.get_media(default_when_empty=default) if HAS_FALCON3 else \ - (req.media if req.media else default) - - -def get_response_text(resp): - # in falcon 3 body is deprecated - return getattr(resp, 'text') if HAS_FALCON3 else \ - getattr(resp, 'body') - - -def set_response_text(resp, text): - # in falcon 3 body is deprecated - setattr(resp, 'text', text) if HAS_FALCON3 else \ - setattr(resp, 'body', text) diff --git a/openapi_core/contrib/falcon/handlers.py b/openapi_core/contrib/falcon/handlers.py index 671cb7a7..5b48404d 100644 --- a/openapi_core/contrib/falcon/handlers.py +++ b/openapi_core/contrib/falcon/handlers.py @@ -6,7 +6,6 @@ HTTP_400, HTTP_404, HTTP_405, HTTP_415, ) -from openapi_core.contrib.falcon.compat import set_response_text from openapi_core.templating.media_types.exceptions import MediaTypeNotFound from openapi_core.templating.paths.exceptions import ( ServerNotFound, OperationNotFound, PathNotFound, @@ -43,7 +42,7 @@ def handle(cls, req, resp, errors): resp.content_type = MEDIA_JSON resp.status = cls.FALCON_STATUS_CODES.get( data_error_max['status'], HTTP_400) - set_response_text(resp, data_str) + resp.text = data_str resp.complete = True @classmethod diff --git a/openapi_core/contrib/falcon/middlewares.py b/openapi_core/contrib/falcon/middlewares.py index e809cc7c..2e151975 100644 --- a/openapi_core/contrib/falcon/middlewares.py +++ b/openapi_core/contrib/falcon/middlewares.py @@ -8,24 +8,46 @@ from openapi_core.validation.response.validators import ResponseValidator -class FalconOpenAPIMiddleware(OpenAPIProcessor): +class FalconOpenAPIMiddleware: + + request_factory = FalconOpenAPIRequestFactory() + response_factory = FalconOpenAPIResponseFactory() + errors_handler = FalconOpenAPIErrorsHandler() def __init__( - self, - request_validator, - response_validator, - request_factory, - response_factory, - openapi_errors_handler, + self, + validation_processor, + request_factory=None, + response_factory=None, + errors_handler=None, + ): + self.validation_processor = validation_processor + self.request_factory = request_factory or self.request_factory + self.response_factory = response_factory or self.response_factory + self.errors_handler = errors_handler or self.errors_handler + + @classmethod + def from_spec( + cls, + spec, + request_factory=None, + response_factory=None, + errors_handler=None, ): - super().__init__(request_validator, response_validator) - self.request_factory = request_factory - self.response_factory = response_factory - self.openapi_errors_handler = openapi_errors_handler + request_validator = RequestValidator(spec) + response_validator = ResponseValidator(spec) + validation_processor = OpenAPIProcessor( + request_validator, response_validator) + return cls( + validation_processor, + request_factory=request_factory, + response_factory=response_factory, + errors_handler=errors_handler, + ) def process_request(self, req, resp): openapi_req = self._get_openapi_request(req) - req_result = super().process_request(openapi_req) + req_result = self.validation_processor.process_request(openapi_req) if req_result.errors: return self._handle_request_errors(req, resp, req_result) req.openapi = req_result @@ -33,16 +55,17 @@ def process_request(self, req, resp): def process_response(self, req, resp, resource, req_succeeded): openapi_req = self._get_openapi_request(req) openapi_resp = self._get_openapi_response(resp) - resp_result = super().process_response(openapi_req, openapi_resp) + resp_result = self.validation_processor.process_response( + openapi_req, openapi_resp) if resp_result.errors: return self._handle_response_errors(req, resp, resp_result) def _handle_request_errors(self, req, resp, request_result): - return self.openapi_errors_handler.handle( + return self.errors_handler.handle( req, resp, request_result.errors) def _handle_response_errors(self, req, resp, response_result): - return self.openapi_errors_handler.handle( + return self.errors_handler.handle( req, resp, response_result.errors) def _get_openapi_request(self, request): @@ -50,21 +73,3 @@ def _get_openapi_request(self, request): def _get_openapi_response(self, response): return self.response_factory.create(response) - - @classmethod - def from_spec( - cls, - spec, - request_factory=FalconOpenAPIRequestFactory, - response_factory=FalconOpenAPIResponseFactory, - openapi_errors_handler=FalconOpenAPIErrorsHandler, - ): - request_validator = RequestValidator(spec) - response_validator = ResponseValidator(spec) - return cls( - request_validator=request_validator, - response_validator=response_validator, - request_factory=request_factory, - response_factory=response_factory, - openapi_errors_handler=openapi_errors_handler, - ) diff --git a/openapi_core/contrib/falcon/requests.py b/openapi_core/contrib/falcon/requests.py index 9f6b5292..49fe2fe9 100644 --- a/openapi_core/contrib/falcon/requests.py +++ b/openapi_core/contrib/falcon/requests.py @@ -3,7 +3,6 @@ from werkzeug.datastructures import ImmutableMultiDict, Headers -from openapi_core.contrib.falcon.compat import get_request_media from openapi_core.validation.request.datatypes import ( OpenAPIRequest, RequestParameters, ) @@ -11,15 +10,18 @@ class FalconOpenAPIRequestFactory: - @classmethod - def create(cls, request, default_when_empty={}): + def __init__(self, default_when_empty=None): + if default_when_empty is None: + default_when_empty = {} + self.default_when_empty = default_when_empty + + def create(self, request): """ Create OpenAPIRequest from falcon Request and route params. """ - default = default_when_empty method = request.method.lower() - media = get_request_media(request, default=default) + media = request.get_media(default_when_empty=self.default_when_empty) # Support falcon-jsonify. body = ( dumps(getattr(request, "json", media)) diff --git a/openapi_core/contrib/falcon/responses.py b/openapi_core/contrib/falcon/responses.py index f99da684..9e90af43 100644 --- a/openapi_core/contrib/falcon/responses.py +++ b/openapi_core/contrib/falcon/responses.py @@ -1,7 +1,6 @@ """OpenAPI core contrib falcon responses module""" from werkzeug.datastructures import Headers -from openapi_core.contrib.falcon.compat import get_response_text from openapi_core.validation.response.datatypes import OpenAPIResponse @@ -16,7 +15,7 @@ def create(cls, response): else: mimetype = response.options.default_media_type - data = get_response_text(response) + data = response.text headers = Headers(response.headers) return OpenAPIResponse( diff --git a/requirements_dev.txt b/requirements_dev.txt index fb38e977..7bebefda 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,7 +1,7 @@ pytest==5.4.3 pytest-flake8 pytest-cov==2.5.1 -falcon==3.0.0 +falcon==3.0.1 flask django==2.2.20 djangorestframework==3.11.2 diff --git a/setup.cfg b/setup.cfg index 7de244ae..1d775e51 100644 --- a/setup.cfg +++ b/setup.cfg @@ -37,7 +37,7 @@ tests_require = pytest>=5.0.0 pytest-flake8 pytest-cov - falcon + falcon>=3.0 flask responses webob @@ -48,6 +48,7 @@ exclude = [options.extras_require] django = django>=2.2 +falcon = falcon>=3.0 flask = flask requests = requests diff --git a/tests/integration/contrib/falcon/conftest.py b/tests/integration/contrib/falcon/conftest.py index 66d209de..2d8c255c 100644 --- a/tests/integration/contrib/falcon/conftest.py +++ b/tests/integration/contrib/falcon/conftest.py @@ -1,7 +1,10 @@ +import os +import sys + from falcon import Request, Response, RequestOptions, ResponseOptions from falcon.routing import DefaultRouter from falcon.status_codes import HTTP_200 -from falcon.testing import create_environ +from falcon.testing import create_environ, TestClient import pytest @@ -50,3 +53,23 @@ def create_response( resp.set_headers(headers or {}) return resp return create_response + + +@pytest.fixture(autouse=True, scope='module') +def falcon_setup(): + directory = os.path.abspath(os.path.dirname(__file__)) + falcon_project_dir = os.path.join(directory, 'data/v3.0') + sys.path.insert(0, falcon_project_dir) + yield + sys.path.remove(falcon_project_dir) + + +@pytest.fixture +def app(): + from falconproject.__main__ import app + return app + + +@pytest.fixture +def client(app): + return TestClient(app) diff --git a/tests/integration/contrib/falcon/data/v3.0/falconproject/__init__.py b/tests/integration/contrib/falcon/data/v3.0/falconproject/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/contrib/falcon/data/v3.0/falconproject/__main__.py b/tests/integration/contrib/falcon/data/v3.0/falconproject/__main__.py new file mode 100644 index 00000000..fc9d5e75 --- /dev/null +++ b/tests/integration/contrib/falcon/data/v3.0/falconproject/__main__.py @@ -0,0 +1,12 @@ +from falcon import App + +from falconproject.openapi import openapi_middleware +from falconproject.resources import PetListResource, PetDetailResource + +app = App(middleware=[openapi_middleware]) + +pet_list_resource = PetListResource() +pet_detail_resource = PetDetailResource() + +app.add_route("/v1/pets", pet_list_resource) +app.add_route("/v1/pets/{petId}", pet_detail_resource) diff --git a/tests/integration/contrib/falcon/data/v3.0/falconproject/openapi.py b/tests/integration/contrib/falcon/data/v3.0/falconproject/openapi.py new file mode 100644 index 00000000..eefb3a65 --- /dev/null +++ b/tests/integration/contrib/falcon/data/v3.0/falconproject/openapi.py @@ -0,0 +1,11 @@ +from pathlib import Path + +from openapi_core import create_spec +from openapi_core.contrib.falcon.middlewares import FalconOpenAPIMiddleware +import yaml + +openapi_spec_path = Path("tests/integration/data/v3.0/petstore.yaml") +spec_yaml = openapi_spec_path.read_text() +spec_dict = yaml.load(spec_yaml) +spec = create_spec(spec_dict) +openapi_middleware = FalconOpenAPIMiddleware.from_spec(spec) diff --git a/tests/integration/contrib/falcon/data/v3.0/falconproject/resources.py b/tests/integration/contrib/falcon/data/v3.0/falconproject/resources.py new file mode 100644 index 00000000..cca48515 --- /dev/null +++ b/tests/integration/contrib/falcon/data/v3.0/falconproject/resources.py @@ -0,0 +1,49 @@ +from json import dumps + +from falcon.constants import MEDIA_JSON +from falcon.status_codes import HTTP_200 + + +class PetListResource: + def on_get(self, request, response): + assert request.openapi + assert not request.openapi.errors + assert request.openapi.parameters.query == { + 'page': 1, + 'limit': 12, + 'search': '', + } + data = [ + { + 'id': 12, + 'name': 'Cat', + 'ears': { + 'healthy': True, + }, + }, + ] + response.status = HTTP_200 + response.content_type = MEDIA_JSON + response.text = dumps({"data": data}) + response.set_header('X-Rate-Limit', '12') + + +class PetDetailResource: + def on_get(self, request, response, petId=None): + assert petId == '12' + assert request.openapi + assert not request.openapi.errors + assert request.openapi.parameters.path == { + 'petId': 12, + } + data = { + 'id': 12, + 'name': 'Cat', + 'ears': { + 'healthy': True, + }, + } + response.status = HTTP_200 + response.content_type = MEDIA_JSON + response.text = dumps({"data": data}) + response.set_header('X-Rate-Limit', '12') diff --git a/tests/integration/contrib/falcon/data/v3.0/falcon_factory.yaml b/tests/integration/contrib/falcon/data/v3.0/openapi.yaml similarity index 99% rename from tests/integration/contrib/falcon/data/v3.0/falcon_factory.yaml rename to tests/integration/contrib/falcon/data/v3.0/openapi.yaml index 295f3670..7646f8fc 100644 --- a/tests/integration/contrib/falcon/data/v3.0/falcon_factory.yaml +++ b/tests/integration/contrib/falcon/data/v3.0/openapi.yaml @@ -21,7 +21,7 @@ paths: type: integer get: responses: - 200: + '200': description: Return the resource. content: application/json: diff --git a/tests/integration/contrib/falcon/test_falcon.py b/tests/integration/contrib/falcon/test_falcon.py new file mode 100644 index 00000000..1706ec07 --- /dev/null +++ b/tests/integration/contrib/falcon/test_falcon.py @@ -0,0 +1,67 @@ +class TestPetListResource: + + def test_no_required_param(self, client): + headers = { + 'Content-Type': 'application/json', + } + + response = client.simulate_get( + '/v1/pets', host='petstore.swagger.io', headers=headers) + + assert response.status_code == 400 + + def test_valid(self, client): + headers = { + 'Content-Type': 'application/json', + } + query_string = "limit=12" + + response = client.simulate_get( + '/v1/pets', + host='petstore.swagger.io', headers=headers, + query_string=query_string, + ) + + assert response.status_code == 200 + assert response.json == { + 'data': [ + { + 'id': 12, + 'name': 'Cat', + 'ears': { + 'healthy': True, + }, + }, + ], + } + + +class TestPetDetailResource: + + def test_invalid_path(self, client): + headers = {'Content-Type': 'application/json'} + + response = client.simulate_get( + '/v1/pet/invalid', host='petstore.swagger.io', headers=headers) + + assert response.status_code == 404 + + def test_invalid_security(self, client): + headers = {'Content-Type': 'application/json'} + + response = client.simulate_get( + '/v1/pets/12', host='petstore.swagger.io', headers=headers) + + assert response.status_code == 400 + + def test_valid(self, client): + auth = 'authuser' + headers = { + 'Authorization': 'Basic {auth}'.format(auth=auth), + 'Content-Type': 'application/json', + } + + response = client.simulate_get( + '/v1/pets/12', host='petstore.swagger.io', headers=headers) + + assert response.status_code == 200 diff --git a/tests/integration/contrib/falcon/test_falcon_middlewares.py b/tests/integration/contrib/falcon/test_falcon_middlewares.py index 354b7eea..f49d792b 100644 --- a/tests/integration/contrib/falcon/test_falcon_middlewares.py +++ b/tests/integration/contrib/falcon/test_falcon_middlewares.py @@ -1,6 +1,6 @@ from json import dumps -from falcon import API as App +from falcon import App from falcon.testing import TestClient import pytest @@ -15,7 +15,7 @@ class TestFalconOpenAPIMiddleware: @pytest.fixture def spec(self, factory): - specfile = 'contrib/falcon/data/v3.0/falcon_factory.yaml' + specfile = 'contrib/falcon/data/v3.0/openapi.yaml' return create_spec(factory.spec_from_file(specfile)) @pytest.fixture diff --git a/tests/integration/contrib/falcon/test_falcon_validation.py b/tests/integration/contrib/falcon/test_falcon_validation.py index 5021497c..f65b690a 100644 --- a/tests/integration/contrib/falcon/test_falcon_validation.py +++ b/tests/integration/contrib/falcon/test_falcon_validation.py @@ -11,7 +11,7 @@ class TestFalconOpenAPIValidation: @pytest.fixture def spec(self, factory): - specfile = 'contrib/falcon/data/v3.0/falcon_factory.yaml' + specfile = 'contrib/falcon/data/v3.0/openapi.yaml' return create_spec(factory.spec_from_file(specfile)) def test_response_validator_path_pattern(self, @@ -20,19 +20,19 @@ def test_response_validator_path_pattern(self, response_factory): validator = ResponseValidator(spec) request = request_factory('GET', '/browse/12', subdomain='kb') - openapi_request = FalconOpenAPIRequestFactory.create(request) + openapi_request = FalconOpenAPIRequestFactory().create(request) response = response_factory( '{"data": "data"}', status_code=200, headers={'X-Rate-Limit': '12'}, ) - openapi_response = FalconOpenAPIResponseFactory.create(response) + openapi_response = FalconOpenAPIResponseFactory().create(response) result = validator.validate(openapi_request, openapi_response) assert not result.errors def test_request_validator_path_pattern(self, spec, request_factory): validator = RequestValidator(spec) request = request_factory('GET', '/browse/12', subdomain='kb') - openapi_request = FalconOpenAPIRequestFactory.create(request) + openapi_request = FalconOpenAPIRequestFactory().create(request) result = validator.validate(openapi_request) assert not result.errors @@ -41,6 +41,6 @@ def test_request_validator_with_query(self, spec, request_factory): request = request_factory('GET', '/browse/12', query_string='detail_level=2', subdomain='kb') - openapi_request = FalconOpenAPIRequestFactory.create(request) + openapi_request = FalconOpenAPIRequestFactory().create(request) result = validator.validate(openapi_request) assert not result.errors pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy