Requests integration

This commit is contained in:
Artur Maciag 2020-03-02 16:05:36 +00:00
parent 02d20e5f44
commit 7e920f8290
15 changed files with 337 additions and 27 deletions

View file

@ -291,6 +291,36 @@ Pyramid
See `pyramid_openapi3 <https://github.com/niteoweb/pyramid_openapi3>`_ project. 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 Related projects
################ ################
* `openapi-spec-validator <https://github.com/p1c2u/openapi-spec-validator>`__ * `openapi-spec-validator <https://github.com/p1c2u/openapi-spec-validator>`__

View 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',
]

View 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,
)

View 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,
)

View file

@ -10,19 +10,19 @@ class RequestParameters(object):
"""OpenAPI request parameters dataclass. """OpenAPI request parameters dataclass.
Attributes: Attributes:
path
Path parameters as dict.
query query
Query string parameters as MultiDict. Must support getlist method. Query string parameters as MultiDict. Must support getlist method.
header header
Request headers as dict. Request headers as dict.
cookie cookie
Request cookies as dict. 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) query = attr.ib(factory=ImmutableMultiDict)
header = attr.ib(factory=dict) header = attr.ib(factory=dict)
cookie = attr.ib(factory=dict) cookie = attr.ib(factory=dict)
path = attr.ib(factory=dict)
def __getitem__(self, location): def __getitem__(self, location):
return getattr(self, location) return getattr(self, location)
@ -63,3 +63,6 @@ class RequestValidationResult(BaseValidationResult):
body = attr.ib(default=None) body = attr.ib(default=None)
parameters = attr.ib(factory=RequestParameters) parameters = attr.ib(factory=RequestParameters)
security = attr.ib(default=None) security = attr.ib(default=None)
server = attr.ib(default=None)
path = attr.ib(default=None)
operation = attr.ib(default=None)

View file

@ -26,16 +26,18 @@ class RequestValidator(BaseValidator):
def validate(self, request): def validate(self, request):
try: try:
path, operation, _, _, _ = self._find_path(request) path, operation, _, path_result, _ = self._find_path(request)
# don't process if operation errors # don't process if operation errors
except PathError as exc: except PathError as exc:
return RequestValidationResult([exc, ], None, None, None) return RequestValidationResult(errors=[exc, ])
try: try:
security = self._get_security(request, operation) security = self._get_security(request, operation)
except InvalidSecurity as exc: 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( params, params_errors = self._get_parameters(
request, chain( request, chain(
iteritems(operation.parameters), iteritems(operation.parameters),
@ -46,30 +48,43 @@ class RequestValidator(BaseValidator):
body, body_errors = self._get_body(request, operation) body, body_errors = self._get_body(request, operation)
errors = params_errors + body_errors 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): def _validate_parameters(self, request):
try: try:
path, operation, _, _, _ = self._find_path(request) path, operation, _, path_result, _ = self._find_path(request)
except PathError as exc: 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( params, params_errors = self._get_parameters(
request, chain( request, chain(
iteritems(operation.parameters), iteritems(operation.parameters),
iteritems(path.parameters) iteritems(path.parameters)
) )
) )
return RequestValidationResult(params_errors, None, params, None) return RequestValidationResult(
errors=params_errors,
parameters=params,
)
def _validate_body(self, request): def _validate_body(self, request):
try: try:
_, operation, _, _, _ = self._find_path(request) _, operation, _, _, _ = self._find_path(request)
except PathError as exc: except PathError as exc:
return RequestValidationResult([exc, ], None, None) return RequestValidationResult(errors=[exc, ])
body, body_errors = self._get_body(request, operation) 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): def _get_security(self, request, operation):
security = operation.security or self.spec.security security = operation.security or self.spec.security

View file

@ -21,14 +21,14 @@ class ResponseValidator(BaseValidator):
_, operation, _, _, _ = self._find_path(request) _, operation, _, _, _ = self._find_path(request)
# don't process if operation errors # don't process if operation errors
except PathError as exc: except PathError as exc:
return ResponseValidationResult([exc, ], None, None) return ResponseValidationResult(errors=[exc, ])
try: try:
operation_response = self._get_operation_response( operation_response = self._get_operation_response(
operation, response) operation, response)
# don't process if operation errors # don't process if operation errors
except InvalidResponse as exc: except InvalidResponse as exc:
return ResponseValidationResult([exc, ], None, None) return ResponseValidationResult(errors=[exc, ])
data, data_errors = self._get_data(response, operation_response) data, data_errors = self._get_data(response, operation_response)
@ -36,7 +36,11 @@ class ResponseValidator(BaseValidator):
response, operation_response) response, operation_response)
errors = data_errors + headers_errors 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): def _get_operation_response(self, operation, response):
return operation.get_response(str(response.status_code)) return operation.get_response(str(response.status_code))
@ -46,17 +50,20 @@ class ResponseValidator(BaseValidator):
_, operation, _, _, _ = self._find_path(request) _, operation, _, _, _ = self._find_path(request)
# don't process if operation errors # don't process if operation errors
except PathError as exc: except PathError as exc:
return ResponseValidationResult([exc, ], None, None) return ResponseValidationResult(errors=[exc, ])
try: try:
operation_response = self._get_operation_response( operation_response = self._get_operation_response(
operation, response) operation, response)
# don't process if operation errors # don't process if operation errors
except InvalidResponse as exc: except InvalidResponse as exc:
return ResponseValidationResult([exc, ], None, None) return ResponseValidationResult(errors=[exc, ])
data, data_errors = self._get_data(response, operation_response) 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): def _get_data(self, response, operation_response):
if not operation_response.content: if not operation_response.content:

View file

@ -4,4 +4,5 @@ pytest-flake8
pytest-cov==2.5.1 pytest-cov==2.5.1
flask flask
django==2.2.10; python_version>="3.0" django==2.2.10; python_version>="3.0"
requests==2.22.0
webob webob

View file

@ -49,6 +49,7 @@ exclude =
[options.extras_require] [options.extras_require]
django = django>=2.2; python_version>="3.0" django = django>=2.2; python_version>="3.0"
flask = flask flask = flask
requests = requests
[tool:pytest] [tool:pytest]
addopts = -sv --flake8 --junitxml reports/junit.xml --cov openapi_core --cov-report term-missing --cov-report xml:reports/coverage.xml addopts = -sv --flake8 --junitxml reports/junit.xml --cov openapi_core --cov-report term-missing --cov-report xml:reports/coverage.xml

View 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

View file

@ -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

View 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'

View file

@ -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

View file

@ -5,6 +5,7 @@ from openapi_core.templating.paths.exceptions import (
PathNotFound, OperationNotFound, PathNotFound, OperationNotFound,
) )
from openapi_core.testing import MockRequest from openapi_core.testing import MockRequest
from openapi_core.validation.request.datatypes import RequestParameters
from openapi_core.validation.request.validators import RequestValidator from openapi_core.validation.request.validators import RequestValidator
@ -48,7 +49,7 @@ class TestMinimal(object):
assert len(result.errors) == 1 assert len(result.errors) == 1
assert isinstance(result.errors[0], OperationNotFound) assert isinstance(result.errors[0], OperationNotFound)
assert result.body is None assert result.body is None
assert result.parameters is None assert result.parameters == RequestParameters()
@pytest.mark.parametrize("server", servers) @pytest.mark.parametrize("server", servers)
@pytest.mark.parametrize("spec_path", spec_paths) @pytest.mark.parametrize("spec_path", spec_paths)
@ -63,4 +64,4 @@ class TestMinimal(object):
assert len(result.errors) == 1 assert len(result.errors) == 1
assert isinstance(result.errors[0], PathNotFound) assert isinstance(result.errors[0], PathNotFound)
assert result.body is None assert result.body is None
assert result.parameters is None assert result.parameters == RequestParameters()

View file

@ -58,7 +58,7 @@ class TestRequestValidator(object):
assert len(result.errors) == 1 assert len(result.errors) == 1
assert type(result.errors[0]) == PathNotFound assert type(result.errors[0]) == PathNotFound
assert result.body is None assert result.body is None
assert result.parameters is None assert result.parameters == RequestParameters()
def test_invalid_path(self, validator): def test_invalid_path(self, validator):
request = MockRequest(self.host_url, 'get', '/v1') request = MockRequest(self.host_url, 'get', '/v1')
@ -68,7 +68,7 @@ class TestRequestValidator(object):
assert len(result.errors) == 1 assert len(result.errors) == 1
assert type(result.errors[0]) == PathNotFound assert type(result.errors[0]) == PathNotFound
assert result.body is None assert result.body is None
assert result.parameters is None assert result.parameters == RequestParameters()
def test_invalid_operation(self, validator): def test_invalid_operation(self, validator):
request = MockRequest(self.host_url, 'patch', '/v1/pets') request = MockRequest(self.host_url, 'patch', '/v1/pets')
@ -78,7 +78,7 @@ class TestRequestValidator(object):
assert len(result.errors) == 1 assert len(result.errors) == 1
assert type(result.errors[0]) == OperationNotFound assert type(result.errors[0]) == OperationNotFound
assert result.body is None assert result.body is None
assert result.parameters is None assert result.parameters == RequestParameters()
def test_missing_parameter(self, validator): def test_missing_parameter(self, validator):
request = MockRequest(self.host_url, 'get', '/v1/pets') request = MockRequest(self.host_url, 'get', '/v1/pets')
@ -259,7 +259,7 @@ class TestRequestValidator(object):
assert result.errors == [InvalidSecurity(), ] assert result.errors == [InvalidSecurity(), ]
assert result.body is None assert result.body is None
assert result.parameters is None assert result.parameters == RequestParameters()
assert result.security is None assert result.security is None
def test_get_pet(self, validator): def test_get_pet(self, validator):
@ -432,7 +432,7 @@ class TestResponseValidator(object):
assert len(result.errors) == 1 assert len(result.errors) == 1
assert type(result.errors[0]) == PathNotFound assert type(result.errors[0]) == PathNotFound
assert result.data is None assert result.data is None
assert result.headers is None assert result.headers == {}
def test_invalid_operation(self, validator): def test_invalid_operation(self, validator):
request = MockRequest(self.host_url, 'patch', '/v1/pets') request = MockRequest(self.host_url, 'patch', '/v1/pets')
@ -443,7 +443,7 @@ class TestResponseValidator(object):
assert len(result.errors) == 1 assert len(result.errors) == 1
assert type(result.errors[0]) == OperationNotFound assert type(result.errors[0]) == OperationNotFound
assert result.data is None assert result.data is None
assert result.headers is None assert result.headers == {}
def test_invalid_response(self, validator): def test_invalid_response(self, validator):
request = MockRequest(self.host_url, 'get', '/v1/pets') request = MockRequest(self.host_url, 'get', '/v1/pets')
@ -454,7 +454,7 @@ class TestResponseValidator(object):
assert len(result.errors) == 1 assert len(result.errors) == 1
assert type(result.errors[0]) == InvalidResponse assert type(result.errors[0]) == InvalidResponse
assert result.data is None assert result.data is None
assert result.headers is None assert result.headers == {}
def test_invalid_content_type(self, validator): def test_invalid_content_type(self, validator):
request = MockRequest(self.host_url, 'get', '/v1/pets') request = MockRequest(self.host_url, 'get', '/v1/pets')