mirror of
https://github.com/correl/openapi-core.git
synced 2024-12-29 11:09:25 +00:00
Merge pull request #209 from p1c2u/feature/requests-integration
Requests integration
This commit is contained in:
commit
c27cd892ec
15 changed files with 337 additions and 27 deletions
30
README.rst
30
README.rst
|
@ -291,6 +291,36 @@ Pyramid
|
|||
|
||||
See `pyramid_openapi3 <https://github.com/niteoweb/pyramid_openapi3>`_ project.
|
||||
|
||||
Requests
|
||||
********
|
||||
|
||||
This section describes integration with `Requests <https://requests.readthedocs.io>`__ library.
|
||||
|
||||
Low level
|
||||
=========
|
||||
|
||||
For Requests you can use RequestsOpenAPIRequest a Requests request factory:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from openapi_core.validation.request.validators import RequestValidator
|
||||
from openapi_core.contrib.requests import RequestsOpenAPIRequest
|
||||
|
||||
openapi_request = RequestsOpenAPIRequest(requests_request)
|
||||
validator = RequestValidator(spec)
|
||||
result = validator.validate(openapi_request)
|
||||
|
||||
You can use RequestsOpenAPIResponse as a Requests response factory:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from openapi_core.validation.response.validators import ResponseValidator
|
||||
from openapi_core.contrib.requests import RequestsOpenAPIResponse
|
||||
|
||||
openapi_response = RequestsOpenAPIResponse(requests_response)
|
||||
validator = ResponseValidator(spec)
|
||||
result = validator.validate(openapi_request, openapi_response)
|
||||
|
||||
Related projects
|
||||
################
|
||||
* `openapi-spec-validator <https://github.com/p1c2u/openapi-spec-validator>`__
|
||||
|
|
15
openapi_core/contrib/requests/__init__.py
Normal file
15
openapi_core/contrib/requests/__init__.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
from openapi_core.contrib.requests.requests import (
|
||||
RequestsOpenAPIRequestFactory,
|
||||
)
|
||||
from openapi_core.contrib.requests.responses import (
|
||||
RequestsOpenAPIResponseFactory,
|
||||
)
|
||||
|
||||
# backward compatibility
|
||||
RequestsOpenAPIRequest = RequestsOpenAPIRequestFactory.create
|
||||
RequestsOpenAPIResponse = RequestsOpenAPIResponseFactory.create
|
||||
|
||||
__all__ = [
|
||||
'RequestsOpenAPIRequestFactory', 'RequestsOpenAPIResponseFactory',
|
||||
'RequestsOpenAPIRequest', 'RequestsOpenAPIResponse',
|
||||
]
|
34
openapi_core/contrib/requests/requests.py
Normal file
34
openapi_core/contrib/requests/requests.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
"""OpenAPI core contrib requests requests module"""
|
||||
from werkzeug.datastructures import ImmutableMultiDict
|
||||
|
||||
from openapi_core.validation.request.datatypes import (
|
||||
RequestParameters, OpenAPIRequest,
|
||||
)
|
||||
|
||||
|
||||
class RequestsOpenAPIRequestFactory(object):
|
||||
|
||||
@classmethod
|
||||
def create(cls, request):
|
||||
method = request.method.lower()
|
||||
|
||||
cookie = request.cookies or {}
|
||||
|
||||
# gets deduced by path finder against spec
|
||||
path = {}
|
||||
|
||||
mimetype = request.headers.get('Accept') or \
|
||||
request.headers.get('Content-Type')
|
||||
parameters = RequestParameters(
|
||||
query=ImmutableMultiDict(request.params),
|
||||
header=request.headers,
|
||||
cookie=cookie,
|
||||
path=path,
|
||||
)
|
||||
return OpenAPIRequest(
|
||||
full_url_pattern=request.url,
|
||||
method=method,
|
||||
parameters=parameters,
|
||||
body=request.data,
|
||||
mimetype=mimetype,
|
||||
)
|
14
openapi_core/contrib/requests/responses.py
Normal file
14
openapi_core/contrib/requests/responses.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
"""OpenAPI core contrib requests responses module"""
|
||||
from openapi_core.validation.response.datatypes import OpenAPIResponse
|
||||
|
||||
|
||||
class RequestsOpenAPIResponseFactory(object):
|
||||
|
||||
@classmethod
|
||||
def create(cls, response):
|
||||
mimetype = response.headers.get('Content-Type')
|
||||
return OpenAPIResponse(
|
||||
data=response.raw,
|
||||
status_code=response.status_code,
|
||||
mimetype=mimetype,
|
||||
)
|
|
@ -10,19 +10,19 @@ class RequestParameters(object):
|
|||
"""OpenAPI request parameters dataclass.
|
||||
|
||||
Attributes:
|
||||
path
|
||||
Path parameters as dict.
|
||||
query
|
||||
Query string parameters as MultiDict. Must support getlist method.
|
||||
header
|
||||
Request headers as dict.
|
||||
cookie
|
||||
Request cookies as dict.
|
||||
path
|
||||
Path parameters as dict. Gets resolved against spec if empty.
|
||||
"""
|
||||
path = attr.ib(factory=dict)
|
||||
query = attr.ib(factory=ImmutableMultiDict)
|
||||
header = attr.ib(factory=dict)
|
||||
cookie = attr.ib(factory=dict)
|
||||
path = attr.ib(factory=dict)
|
||||
|
||||
def __getitem__(self, location):
|
||||
return getattr(self, location)
|
||||
|
@ -63,3 +63,6 @@ class RequestValidationResult(BaseValidationResult):
|
|||
body = attr.ib(default=None)
|
||||
parameters = attr.ib(factory=RequestParameters)
|
||||
security = attr.ib(default=None)
|
||||
server = attr.ib(default=None)
|
||||
path = attr.ib(default=None)
|
||||
operation = attr.ib(default=None)
|
||||
|
|
|
@ -26,16 +26,18 @@ class RequestValidator(BaseValidator):
|
|||
|
||||
def validate(self, request):
|
||||
try:
|
||||
path, operation, _, _, _ = self._find_path(request)
|
||||
path, operation, _, path_result, _ = self._find_path(request)
|
||||
# don't process if operation errors
|
||||
except PathError as exc:
|
||||
return RequestValidationResult([exc, ], None, None, None)
|
||||
return RequestValidationResult(errors=[exc, ])
|
||||
|
||||
try:
|
||||
security = self._get_security(request, operation)
|
||||
except InvalidSecurity as exc:
|
||||
return RequestValidationResult([exc, ], None, None, None)
|
||||
return RequestValidationResult(errors=[exc, ])
|
||||
|
||||
request.parameters.path = request.parameters.path or \
|
||||
path_result.variables
|
||||
params, params_errors = self._get_parameters(
|
||||
request, chain(
|
||||
iteritems(operation.parameters),
|
||||
|
@ -46,30 +48,43 @@ class RequestValidator(BaseValidator):
|
|||
body, body_errors = self._get_body(request, operation)
|
||||
|
||||
errors = params_errors + body_errors
|
||||
return RequestValidationResult(errors, body, params, security)
|
||||
return RequestValidationResult(
|
||||
errors=errors,
|
||||
body=body,
|
||||
parameters=params,
|
||||
security=security,
|
||||
)
|
||||
|
||||
def _validate_parameters(self, request):
|
||||
try:
|
||||
path, operation, _, _, _ = self._find_path(request)
|
||||
path, operation, _, path_result, _ = self._find_path(request)
|
||||
except PathError as exc:
|
||||
return RequestValidationResult([exc, ], None, None)
|
||||
return RequestValidationResult(errors=[exc, ])
|
||||
|
||||
request.parameters.path = request.parameters.path or \
|
||||
path_result.variables
|
||||
params, params_errors = self._get_parameters(
|
||||
request, chain(
|
||||
iteritems(operation.parameters),
|
||||
iteritems(path.parameters)
|
||||
)
|
||||
)
|
||||
return RequestValidationResult(params_errors, None, params, None)
|
||||
return RequestValidationResult(
|
||||
errors=params_errors,
|
||||
parameters=params,
|
||||
)
|
||||
|
||||
def _validate_body(self, request):
|
||||
try:
|
||||
_, operation, _, _, _ = self._find_path(request)
|
||||
except PathError as exc:
|
||||
return RequestValidationResult([exc, ], None, None)
|
||||
return RequestValidationResult(errors=[exc, ])
|
||||
|
||||
body, body_errors = self._get_body(request, operation)
|
||||
return RequestValidationResult(body_errors, body, None, None)
|
||||
return RequestValidationResult(
|
||||
errors=body_errors,
|
||||
body=body,
|
||||
)
|
||||
|
||||
def _get_security(self, request, operation):
|
||||
security = operation.security or self.spec.security
|
||||
|
|
|
@ -21,14 +21,14 @@ class ResponseValidator(BaseValidator):
|
|||
_, operation, _, _, _ = self._find_path(request)
|
||||
# don't process if operation errors
|
||||
except PathError as exc:
|
||||
return ResponseValidationResult([exc, ], None, None)
|
||||
return ResponseValidationResult(errors=[exc, ])
|
||||
|
||||
try:
|
||||
operation_response = self._get_operation_response(
|
||||
operation, response)
|
||||
# don't process if operation errors
|
||||
except InvalidResponse as exc:
|
||||
return ResponseValidationResult([exc, ], None, None)
|
||||
return ResponseValidationResult(errors=[exc, ])
|
||||
|
||||
data, data_errors = self._get_data(response, operation_response)
|
||||
|
||||
|
@ -36,7 +36,11 @@ class ResponseValidator(BaseValidator):
|
|||
response, operation_response)
|
||||
|
||||
errors = data_errors + headers_errors
|
||||
return ResponseValidationResult(errors, data, headers)
|
||||
return ResponseValidationResult(
|
||||
errors=errors,
|
||||
data=data,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
def _get_operation_response(self, operation, response):
|
||||
return operation.get_response(str(response.status_code))
|
||||
|
@ -46,17 +50,20 @@ class ResponseValidator(BaseValidator):
|
|||
_, operation, _, _, _ = self._find_path(request)
|
||||
# don't process if operation errors
|
||||
except PathError as exc:
|
||||
return ResponseValidationResult([exc, ], None, None)
|
||||
return ResponseValidationResult(errors=[exc, ])
|
||||
|
||||
try:
|
||||
operation_response = self._get_operation_response(
|
||||
operation, response)
|
||||
# don't process if operation errors
|
||||
except InvalidResponse as exc:
|
||||
return ResponseValidationResult([exc, ], None, None)
|
||||
return ResponseValidationResult(errors=[exc, ])
|
||||
|
||||
data, data_errors = self._get_data(response, operation_response)
|
||||
return ResponseValidationResult(data_errors, data, None)
|
||||
return ResponseValidationResult(
|
||||
errors=data_errors,
|
||||
data=data,
|
||||
)
|
||||
|
||||
def _get_data(self, response, operation_response):
|
||||
if not operation_response.content:
|
||||
|
|
|
@ -4,4 +4,5 @@ pytest-flake8
|
|||
pytest-cov==2.5.1
|
||||
flask
|
||||
django==2.2.10; python_version>="3.0"
|
||||
requests==2.22.0
|
||||
webob
|
||||
|
|
|
@ -49,6 +49,7 @@ exclude =
|
|||
[options.extras_require]
|
||||
django = django>=2.2; python_version>="3.0"
|
||||
flask = flask
|
||||
requests = requests
|
||||
|
||||
[tool:pytest]
|
||||
addopts = -sv --flake8 --junitxml reports/junit.xml --cov openapi_core --cov-report term-missing --cov-report xml:reports/coverage.xml
|
||||
|
|
34
tests/integration/contrib/requests/conftest.py
Normal file
34
tests/integration/contrib/requests/conftest.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
import pytest
|
||||
from requests.models import Request, Response
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
from six.moves.urllib.parse import urljoin, parse_qs
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def request_factory():
|
||||
schema = 'http'
|
||||
server_name = 'localhost'
|
||||
|
||||
def create_request(method, path, subdomain=None, query_string=''):
|
||||
base_url = '://'.join([schema, server_name])
|
||||
url = urljoin(base_url, path)
|
||||
params = parse_qs(query_string)
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
return Request(method, url, params=params, headers=headers)
|
||||
return create_request
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def response_factory():
|
||||
def create_response(
|
||||
data, status_code=200, content_type='application/json'):
|
||||
resp = Response()
|
||||
resp.headers = CaseInsensitiveDict({
|
||||
'Content-Type': content_type,
|
||||
})
|
||||
resp.status_code = status_code
|
||||
resp.raw = data
|
||||
return resp
|
||||
return create_response
|
|
@ -0,0 +1,48 @@
|
|||
openapi: "3.0.0"
|
||||
info:
|
||||
title: Basic OpenAPI specification used with requests integration tests
|
||||
version: "0.1"
|
||||
servers:
|
||||
- url: 'http://localhost'
|
||||
paths:
|
||||
'/browse/{id}/':
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
description: the ID of the resource to retrieve
|
||||
schema:
|
||||
type: integer
|
||||
get:
|
||||
responses:
|
||||
200:
|
||||
description: Return the resource.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- data
|
||||
properties:
|
||||
data:
|
||||
type: string
|
||||
default:
|
||||
description: Return errors.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- errors
|
||||
properties:
|
||||
errors:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
code:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
72
tests/integration/contrib/requests/test_requests_requests.py
Normal file
72
tests/integration/contrib/requests/test_requests_requests.py
Normal file
|
@ -0,0 +1,72 @@
|
|||
from werkzeug.datastructures import ImmutableMultiDict
|
||||
|
||||
from openapi_core.contrib.requests import RequestsOpenAPIRequest
|
||||
from openapi_core.validation.request.datatypes import RequestParameters
|
||||
|
||||
|
||||
class TestRequestsOpenAPIRequest(object):
|
||||
|
||||
def test_simple(self, request_factory, request):
|
||||
request = request_factory('GET', '/', subdomain='www')
|
||||
|
||||
openapi_request = RequestsOpenAPIRequest(request)
|
||||
|
||||
path = {}
|
||||
query = ImmutableMultiDict([])
|
||||
headers = request.headers
|
||||
cookies = {}
|
||||
assert openapi_request.parameters == RequestParameters(
|
||||
path=path,
|
||||
query=query,
|
||||
header=headers,
|
||||
cookie=cookies,
|
||||
)
|
||||
assert openapi_request.method == request.method.lower()
|
||||
assert openapi_request.full_url_pattern == 'http://localhost/'
|
||||
assert openapi_request.body == request.data
|
||||
assert openapi_request.mimetype == 'application/json'
|
||||
|
||||
def test_multiple_values(self, request_factory, request):
|
||||
request = request_factory(
|
||||
'GET', '/', subdomain='www', query_string='a=b&a=c')
|
||||
|
||||
openapi_request = RequestsOpenAPIRequest(request)
|
||||
|
||||
path = {}
|
||||
query = ImmutableMultiDict([
|
||||
('a', 'b'), ('a', 'c'),
|
||||
])
|
||||
headers = request.headers
|
||||
cookies = {}
|
||||
assert openapi_request.parameters == RequestParameters(
|
||||
path=path,
|
||||
query=query,
|
||||
header=headers,
|
||||
cookie=cookies,
|
||||
)
|
||||
assert openapi_request.method == request.method.lower()
|
||||
assert openapi_request.full_url_pattern == 'http://localhost/'
|
||||
assert openapi_request.body == request.data
|
||||
assert openapi_request.mimetype == 'application/json'
|
||||
|
||||
def test_url_rule(self, request_factory, request):
|
||||
request = request_factory('GET', '/browse/12/', subdomain='kb')
|
||||
|
||||
openapi_request = RequestsOpenAPIRequest(request)
|
||||
|
||||
# empty when not bound to spec
|
||||
path = {}
|
||||
query = ImmutableMultiDict([])
|
||||
headers = request.headers
|
||||
cookies = {}
|
||||
assert openapi_request.parameters == RequestParameters(
|
||||
path=path,
|
||||
query=query,
|
||||
header=headers,
|
||||
cookie=cookies,
|
||||
)
|
||||
assert openapi_request.method == request.method.lower()
|
||||
assert openapi_request.full_url_pattern == \
|
||||
'http://localhost/browse/12/'
|
||||
assert openapi_request.body == request.data
|
||||
assert openapi_request.mimetype == 'application/json'
|
|
@ -0,0 +1,35 @@
|
|||
import pytest
|
||||
|
||||
from openapi_core.contrib.requests import (
|
||||
RequestsOpenAPIRequest, RequestsOpenAPIResponse,
|
||||
)
|
||||
from openapi_core.shortcuts import create_spec
|
||||
from openapi_core.validation.request.validators import RequestValidator
|
||||
from openapi_core.validation.response.validators import ResponseValidator
|
||||
|
||||
|
||||
class TestFlaskOpenAPIValidation(object):
|
||||
|
||||
@pytest.fixture
|
||||
def spec(self, factory):
|
||||
specfile = 'contrib/requests/data/v3.0/requests_factory.yaml'
|
||||
return create_spec(factory.spec_from_file(specfile))
|
||||
|
||||
def test_response_validator_path_pattern(self,
|
||||
spec,
|
||||
request_factory,
|
||||
response_factory):
|
||||
validator = ResponseValidator(spec)
|
||||
request = request_factory('GET', '/browse/12/', subdomain='kb')
|
||||
openapi_request = RequestsOpenAPIRequest(request)
|
||||
response = response_factory('{"data": "data"}', status_code=200)
|
||||
openapi_response = RequestsOpenAPIResponse(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 = RequestsOpenAPIRequest(request)
|
||||
result = validator.validate(openapi_request)
|
||||
assert not result.errors
|
|
@ -5,6 +5,7 @@ from openapi_core.templating.paths.exceptions import (
|
|||
PathNotFound, OperationNotFound,
|
||||
)
|
||||
from openapi_core.testing import MockRequest
|
||||
from openapi_core.validation.request.datatypes import RequestParameters
|
||||
from openapi_core.validation.request.validators import RequestValidator
|
||||
|
||||
|
||||
|
@ -48,7 +49,7 @@ class TestMinimal(object):
|
|||
assert len(result.errors) == 1
|
||||
assert isinstance(result.errors[0], OperationNotFound)
|
||||
assert result.body is None
|
||||
assert result.parameters is None
|
||||
assert result.parameters == RequestParameters()
|
||||
|
||||
@pytest.mark.parametrize("server", servers)
|
||||
@pytest.mark.parametrize("spec_path", spec_paths)
|
||||
|
@ -63,4 +64,4 @@ class TestMinimal(object):
|
|||
assert len(result.errors) == 1
|
||||
assert isinstance(result.errors[0], PathNotFound)
|
||||
assert result.body is None
|
||||
assert result.parameters is None
|
||||
assert result.parameters == RequestParameters()
|
||||
|
|
|
@ -58,7 +58,7 @@ class TestRequestValidator(object):
|
|||
assert len(result.errors) == 1
|
||||
assert type(result.errors[0]) == PathNotFound
|
||||
assert result.body is None
|
||||
assert result.parameters is None
|
||||
assert result.parameters == RequestParameters()
|
||||
|
||||
def test_invalid_path(self, validator):
|
||||
request = MockRequest(self.host_url, 'get', '/v1')
|
||||
|
@ -68,7 +68,7 @@ class TestRequestValidator(object):
|
|||
assert len(result.errors) == 1
|
||||
assert type(result.errors[0]) == PathNotFound
|
||||
assert result.body is None
|
||||
assert result.parameters is None
|
||||
assert result.parameters == RequestParameters()
|
||||
|
||||
def test_invalid_operation(self, validator):
|
||||
request = MockRequest(self.host_url, 'patch', '/v1/pets')
|
||||
|
@ -78,7 +78,7 @@ class TestRequestValidator(object):
|
|||
assert len(result.errors) == 1
|
||||
assert type(result.errors[0]) == OperationNotFound
|
||||
assert result.body is None
|
||||
assert result.parameters is None
|
||||
assert result.parameters == RequestParameters()
|
||||
|
||||
def test_missing_parameter(self, validator):
|
||||
request = MockRequest(self.host_url, 'get', '/v1/pets')
|
||||
|
@ -259,7 +259,7 @@ class TestRequestValidator(object):
|
|||
|
||||
assert result.errors == [InvalidSecurity(), ]
|
||||
assert result.body is None
|
||||
assert result.parameters is None
|
||||
assert result.parameters == RequestParameters()
|
||||
assert result.security is None
|
||||
|
||||
def test_get_pet(self, validator):
|
||||
|
@ -432,7 +432,7 @@ class TestResponseValidator(object):
|
|||
assert len(result.errors) == 1
|
||||
assert type(result.errors[0]) == PathNotFound
|
||||
assert result.data is None
|
||||
assert result.headers is None
|
||||
assert result.headers == {}
|
||||
|
||||
def test_invalid_operation(self, validator):
|
||||
request = MockRequest(self.host_url, 'patch', '/v1/pets')
|
||||
|
@ -443,7 +443,7 @@ class TestResponseValidator(object):
|
|||
assert len(result.errors) == 1
|
||||
assert type(result.errors[0]) == OperationNotFound
|
||||
assert result.data is None
|
||||
assert result.headers is None
|
||||
assert result.headers == {}
|
||||
|
||||
def test_invalid_response(self, validator):
|
||||
request = MockRequest(self.host_url, 'get', '/v1/pets')
|
||||
|
@ -454,7 +454,7 @@ class TestResponseValidator(object):
|
|||
assert len(result.errors) == 1
|
||||
assert type(result.errors[0]) == InvalidResponse
|
||||
assert result.data is None
|
||||
assert result.headers is None
|
||||
assert result.headers == {}
|
||||
|
||||
def test_invalid_content_type(self, validator):
|
||||
request = MockRequest(self.host_url, 'get', '/v1/pets')
|
||||
|
|
Loading…
Reference in a new issue