Merge pull request #209 from p1c2u/feature/requests-integration

Requests integration
This commit is contained in:
A 2020-03-03 11:16:34 +00:00 committed by GitHub
commit c27cd892ec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
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.
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>`__

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.
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)

View file

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

View file

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

View file

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

View file

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

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

View file

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