Skip to content

Commit aaba8e8

Browse files
committed
Requests integration
1 parent 2f91ea3 commit aaba8e8

File tree

12 files changed

+302
-26
lines changed

12 files changed

+302
-26
lines changed
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from openapi_core.contrib.requests.requests import (
2+
RequestsOpenAPIRequestFactory,
3+
)
4+
from openapi_core.contrib.requests.responses import (
5+
RequestsOpenAPIResponseFactory,
6+
)
7+
8+
# backward compatibility
9+
RequestsOpenAPIRequest = RequestsOpenAPIRequestFactory.create
10+
RequestsOpenAPIResponse = RequestsOpenAPIResponseFactory.create
11+
12+
__all__ = [
13+
'RequestsOpenAPIRequestFactory', 'RequestsOpenAPIResponseFactory',
14+
'RequestsOpenAPIRequest', 'RequestsOpenAPIResponse',
15+
]
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""OpenAPI core contrib requests requests module"""
2+
import re
3+
4+
from six.moves.urllib.parse import urljoin
5+
from werkzeug.datastructures import ImmutableMultiDict
6+
7+
from openapi_core.validation.request.datatypes import (
8+
RequestParameters, OpenAPIRequest,
9+
)
10+
11+
12+
class RequestsOpenAPIRequestFactory(object):
13+
14+
@classmethod
15+
def create(cls, request):
16+
method = request.method.lower()
17+
18+
cookie = request.cookies or {}
19+
20+
mimetype = request.headers.get('Accept') or \
21+
request.headers.get('Content-Type')
22+
parameters = RequestParameters(
23+
path={},
24+
query=ImmutableMultiDict(request.params),
25+
header=request.headers,
26+
cookie=cookie,
27+
)
28+
return OpenAPIRequest(
29+
full_url_pattern=request.url,
30+
method=method,
31+
parameters=parameters,
32+
body=request.data,
33+
mimetype=mimetype,
34+
)
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""OpenAPI core contrib requests responses module"""
2+
from openapi_core.validation.response.datatypes import OpenAPIResponse
3+
4+
5+
class RequestsOpenAPIResponseFactory(object):
6+
7+
@classmethod
8+
def create(cls, response):
9+
mimetype = response.headers.get('Content-Type')
10+
return OpenAPIResponse(
11+
data=response.raw,
12+
status_code=response.status_code,
13+
mimetype=mimetype,
14+
)

openapi_core/validation/request/datatypes.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,19 @@ class RequestParameters(object):
1010
"""OpenAPI request parameters dataclass.
1111
1212
Attributes:
13-
path
14-
Path parameters as dict.
1513
query
1614
Query string parameters as MultiDict. Must support getlist method.
1715
header
1816
Request headers as dict.
1917
cookie
2018
Request cookies as dict.
19+
path
20+
Path parameters as dict. Gets resolved against spec if empty.
2121
"""
22-
path = attr.ib(factory=dict)
2322
query = attr.ib(factory=ImmutableMultiDict)
2423
header = attr.ib(factory=dict)
2524
cookie = attr.ib(factory=dict)
25+
path = attr.ib(factory=dict)
2626

2727
def __getitem__(self, location):
2828
return getattr(self, location)
@@ -63,3 +63,6 @@ class RequestValidationResult(BaseValidationResult):
6363
body = attr.ib(default=None)
6464
parameters = attr.ib(factory=RequestParameters)
6565
security = attr.ib(default=None)
66+
server = attr.ib(default=None)
67+
path = attr.ib(default=None)
68+
operation = attr.ib(default=None)

openapi_core/validation/request/validators.py

+21-8
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,18 @@ class RequestValidator(BaseValidator):
2626

2727
def validate(self, request):
2828
try:
29-
path, operation, _, _, _ = self._find_path(request)
29+
path, operation, _, path_result, _ = self._find_path(request)
3030
# don't process if operation errors
3131
except PathError as exc:
32-
return RequestValidationResult([exc, ], None, None, None)
32+
return RequestValidationResult(errors=[exc, ])
3333

3434
try:
3535
security = self._get_security(request, operation)
3636
except InvalidSecurity as exc:
37-
return RequestValidationResult([exc, ], None, None, None)
37+
return RequestValidationResult(errors=[exc, ])
3838

39+
request.parameters.path = request.parameters.path or \
40+
path_result.variables
3941
params, params_errors = self._get_parameters(
4042
request, chain(
4143
iteritems(operation.parameters),
@@ -46,30 +48,41 @@ def validate(self, request):
4648
body, body_errors = self._get_body(request, operation)
4749

4850
errors = params_errors + body_errors
49-
return RequestValidationResult(errors, body, params, security)
51+
return RequestValidationResult(
52+
errors=errors,
53+
body=body,
54+
parameters=params,
55+
security=security,
56+
)
5057

5158
def _validate_parameters(self, request):
5259
try:
5360
path, operation, _, _, _ = self._find_path(request)
5461
except PathError as exc:
55-
return RequestValidationResult([exc, ], None, None)
62+
return RequestValidationResult(errors=[exc, ])
5663

5764
params, params_errors = self._get_parameters(
5865
request, chain(
5966
iteritems(operation.parameters),
6067
iteritems(path.parameters)
6168
)
6269
)
63-
return RequestValidationResult(params_errors, None, params, None)
70+
return RequestValidationResult(
71+
errors=params_errors,
72+
parameters=params,
73+
)
6474

6575
def _validate_body(self, request):
6676
try:
6777
_, operation, _, _, _ = self._find_path(request)
6878
except PathError as exc:
69-
return RequestValidationResult([exc, ], None, None)
79+
return RequestValidationResult(errors=[exc, ])
7080

7181
body, body_errors = self._get_body(request, operation)
72-
return RequestValidationResult(body_errors, body, None, None)
82+
return RequestValidationResult(
83+
errors=body_errors,
84+
body=body,
85+
)
7386

7487
def _get_security(self, request, operation):
7588
security = operation.security or self.spec.security

openapi_core/validation/response/validators.py

+13-6
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,26 @@ def validate(self, request, response):
2121
_, operation, _, _, _ = self._find_path(request)
2222
# don't process if operation errors
2323
except PathError as exc:
24-
return ResponseValidationResult([exc, ], None, None)
24+
return ResponseValidationResult(errors=[exc, ])
2525

2626
try:
2727
operation_response = self._get_operation_response(
2828
operation, response)
2929
# don't process if operation errors
3030
except InvalidResponse as exc:
31-
return ResponseValidationResult([exc, ], None, None)
31+
return ResponseValidationResult(errors=[exc, ])
3232

3333
data, data_errors = self._get_data(response, operation_response)
3434

3535
headers, headers_errors = self._get_headers(
3636
response, operation_response)
3737

3838
errors = data_errors + headers_errors
39-
return ResponseValidationResult(errors, data, headers)
39+
return ResponseValidationResult(
40+
errors=errors,
41+
data=data,
42+
headers=headers,
43+
)
4044

4145
def _get_operation_response(self, operation, response):
4246
return operation.get_response(str(response.status_code))
@@ -46,17 +50,20 @@ def _validate_data(self, request, response):
4650
_, operation, _, _, _ = self._find_path(request)
4751
# don't process if operation errors
4852
except PathError as exc:
49-
return ResponseValidationResult([exc, ], None, None)
53+
return ResponseValidationResult(errors=[exc, ])
5054

5155
try:
5256
operation_response = self._get_operation_response(
5357
operation, response)
5458
# don't process if operation errors
5559
except InvalidResponse as exc:
56-
return ResponseValidationResult([exc, ], None, None)
60+
return ResponseValidationResult(errors=[exc, ])
5761

5862
data, data_errors = self._get_data(response, operation_response)
59-
return ResponseValidationResult(data_errors, data, None)
63+
return ResponseValidationResult(
64+
errors=data_errors,
65+
data=data,
66+
)
6067

6168
def _get_data(self, response, operation_response):
6269
if not operation_response.content:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import pytest
2+
from requests.models import Request, Response
3+
from requests.structures import CaseInsensitiveDict
4+
from six.moves.urllib.parse import urljoin, parse_qs
5+
6+
7+
@pytest.fixture
8+
def request_factory():
9+
schema = 'http'
10+
server_name = 'localhost'
11+
12+
def create_request(method, path, subdomain=None, query_string=''):
13+
base_url = '://'.join([schema, server_name])
14+
url = urljoin(base_url, path)
15+
params = parse_qs(query_string)
16+
headers = {
17+
'Content-Type': 'application/json',
18+
}
19+
return Request(method, url, params=params, headers=headers)
20+
return create_request
21+
22+
23+
@pytest.fixture
24+
def response_factory():
25+
def create_response(
26+
data, status_code=200, content_type='application/json'):
27+
resp = Response()
28+
resp.headers = CaseInsensitiveDict({
29+
'Content-Type': content_type,
30+
})
31+
resp.status_code = status_code
32+
resp.raw = data
33+
return resp
34+
return create_response
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
openapi: "3.0.0"
2+
info:
3+
title: Basic OpenAPI specification used with requests integration tests
4+
version: "0.1"
5+
servers:
6+
- url: 'http://localhost'
7+
paths:
8+
'/browse/{id}/':
9+
parameters:
10+
- name: id
11+
in: path
12+
required: true
13+
description: the ID of the resource to retrieve
14+
schema:
15+
type: integer
16+
get:
17+
responses:
18+
200:
19+
description: Return the resource.
20+
content:
21+
application/json:
22+
schema:
23+
type: object
24+
required:
25+
- data
26+
properties:
27+
data:
28+
type: string
29+
default:
30+
description: Return errors.
31+
content:
32+
application/json:
33+
schema:
34+
type: object
35+
required:
36+
- errors
37+
properties:
38+
errors:
39+
type: array
40+
items:
41+
type: object
42+
properties:
43+
title:
44+
type: string
45+
code:
46+
type: string
47+
message:
48+
type: string
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from werkzeug.datastructures import ImmutableMultiDict
2+
3+
from openapi_core.contrib.requests import RequestsOpenAPIRequest
4+
from openapi_core.validation.request.datatypes import RequestParameters
5+
6+
7+
class TestRequestsOpenAPIRequest(object):
8+
9+
def test_simple(self, request_factory, request):
10+
request = request_factory('GET', '/', subdomain='www')
11+
12+
openapi_request = RequestsOpenAPIRequest(request)
13+
14+
path = {}
15+
query = ImmutableMultiDict([])
16+
headers = request.headers
17+
cookies = {}
18+
assert openapi_request.parameters == RequestParameters(
19+
path=path,
20+
query=query,
21+
header=headers,
22+
cookie=cookies,
23+
)
24+
assert openapi_request.method == request.method.lower()
25+
assert openapi_request.full_url_pattern == 'http://localhost/'
26+
assert openapi_request.body == request.data
27+
assert openapi_request.mimetype == 'application/json'
28+
29+
def test_multiple_values(self, request_factory, request):
30+
request = request_factory(
31+
'GET', '/', subdomain='www', query_string='a=b&a=c')
32+
33+
openapi_request = RequestsOpenAPIRequest(request)
34+
35+
path = {}
36+
query = ImmutableMultiDict([
37+
('a', 'b'), ('a', 'c'),
38+
])
39+
headers = request.headers
40+
cookies = {}
41+
assert openapi_request.parameters == RequestParameters(
42+
path=path,
43+
query=query,
44+
header=headers,
45+
cookie=cookies,
46+
)
47+
assert openapi_request.method == request.method.lower()
48+
assert openapi_request.full_url_pattern == 'http://localhost/'
49+
assert openapi_request.body == request.data
50+
assert openapi_request.mimetype == 'application/json'
51+
52+
def test_url_rule(self, request_factory, request):
53+
request = request_factory('GET', '/browse/12/', subdomain='kb')
54+
55+
openapi_request = RequestsOpenAPIRequest(request)
56+
57+
# empty when not bound to spec
58+
path = {}
59+
query = ImmutableMultiDict([])
60+
headers = request.headers
61+
cookies = {}
62+
assert openapi_request.parameters == RequestParameters(
63+
path=path,
64+
query=query,
65+
header=headers,
66+
cookie=cookies,
67+
)
68+
assert openapi_request.method == request.method.lower()
69+
assert openapi_request.full_url_pattern == \
70+
'http://localhost/browse/12/'
71+
assert openapi_request.body == request.data
72+
assert openapi_request.mimetype == 'application/json'

0 commit comments

Comments
 (0)