Skip to content

Commit 10fdbea

Browse files
committed
Response binary format support
1 parent 0031ff7 commit 10fdbea

File tree

24 files changed

+109
-91
lines changed

24 files changed

+109
-91
lines changed

Diff for: openapi_core/contrib/aiohttp/responses.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ def __init__(self, response: web.Response):
1313
self.response = response
1414

1515
@property
16-
def data(self) -> str:
16+
def data(self) -> bytes:
1717
if self.response.body is None:
18-
return ""
18+
return b""
1919
if isinstance(self.response.body, bytes):
20-
return self.response.body.decode("utf-8")
20+
return self.response.body
2121
assert isinstance(self.response.body, str)
22-
return self.response.body
22+
return self.response.body.encode("utf-8")
2323

2424
@property
2525
def status_code(self) -> int:

Diff for: openapi_core/contrib/django/responses.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
11
"""OpenAPI core contrib django responses module"""
2+
from itertools import tee
3+
24
from django.http.response import HttpResponse
5+
from django.http.response import StreamingHttpResponse
36
from werkzeug.datastructures import Headers
47

58

69
class DjangoOpenAPIResponse:
710
def __init__(self, response: HttpResponse):
8-
if not isinstance(response, HttpResponse):
11+
if not isinstance(response, (HttpResponse, StreamingHttpResponse)):
912
raise TypeError(
10-
f"'response' argument is not type of {HttpResponse}"
13+
f"'response' argument is not type of {HttpResponse} or {StreamingHttpResponse}"
1114
)
1215
self.response = response
1316

1417
@property
15-
def data(self) -> str:
18+
def data(self) -> bytes:
19+
if isinstance(self.response, StreamingHttpResponse):
20+
resp_iter1, resp_iter2 = tee(self.response._iterator)
21+
self.response.streaming_content = resp_iter1
22+
content = b"".join(map(self.response.make_bytes, resp_iter2))
23+
return content
1624
assert isinstance(self.response.content, bytes)
17-
return self.response.content.decode("utf-8")
25+
return self.response.content
1826

1927
@property
2028
def status_code(self) -> int:

Diff for: openapi_core/contrib/falcon/responses.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
"""OpenAPI core contrib falcon responses module"""
2+
from itertools import tee
3+
24
from falcon.response import Response
35
from werkzeug.datastructures import Headers
46

@@ -10,11 +12,16 @@ def __init__(self, response: Response):
1012
self.response = response
1113

1214
@property
13-
def data(self) -> str:
15+
def data(self) -> bytes:
1416
if self.response.text is None:
15-
return ""
17+
if self.response.stream is None:
18+
return b""
19+
resp_iter1, resp_iter2 = tee(self.response.stream)
20+
self.response.stream = resp_iter1
21+
content = b"".join(resp_iter2)
22+
return content
1623
assert isinstance(self.response.text, str)
17-
return self.response.text
24+
return self.response.text.encode("utf-8")
1825

1926
@property
2027
def status_code(self) -> int:

Diff for: openapi_core/contrib/requests/responses.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ def __init__(self, response: Response):
1010
self.response = response
1111

1212
@property
13-
def data(self) -> str:
13+
def data(self) -> bytes:
1414
assert isinstance(self.response.content, bytes)
15-
return self.response.content.decode("utf-8")
15+
return self.response.content
1616

1717
@property
1818
def status_code(self) -> int:

Diff for: openapi_core/contrib/starlette/responses.py

+15-4
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,31 @@
11
"""OpenAPI core contrib starlette responses module"""
2+
from typing import Optional
3+
24
from starlette.datastructures import Headers
35
from starlette.responses import Response
6+
from starlette.responses import StreamingResponse
47

58

69
class StarletteOpenAPIResponse:
7-
def __init__(self, response: Response):
10+
def __init__(self, response: Response, data: Optional[bytes] = None):
811
if not isinstance(response, Response):
912
raise TypeError(f"'response' argument is not type of {Response}")
1013
self.response = response
1114

15+
if data is None and isinstance(response, StreamingResponse):
16+
raise RuntimeError(
17+
f"'data' argument is required for {StreamingResponse}"
18+
)
19+
self._data = data
20+
1221
@property
13-
def data(self) -> str:
22+
def data(self) -> bytes:
23+
if self._data is not None:
24+
return self._data
1425
if isinstance(self.response.body, bytes):
15-
return self.response.body.decode("utf-8")
26+
return self.response.body
1627
assert isinstance(self.response.body, str)
17-
return self.response.body
28+
return self.response.body.encode("utf-8")
1829

1930
@property
2031
def status_code(self) -> int:

Diff for: openapi_core/contrib/werkzeug/responses.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
"""OpenAPI core contrib werkzeug responses module"""
2+
from itertools import tee
3+
24
from werkzeug.datastructures import Headers
35
from werkzeug.wrappers import Response
46

@@ -10,8 +12,12 @@ def __init__(self, response: Response):
1012
self.response = response
1113

1214
@property
13-
def data(self) -> str:
14-
return self.response.get_data(as_text=True)
15+
def data(self) -> bytes:
16+
if not self.response.is_sequence:
17+
resp_iter1, resp_iter2 = tee(self.response.iter_encoded())
18+
self.response.response = resp_iter1
19+
return b"".join(resp_iter2)
20+
return self.response.get_data(as_text=False)
1521

1622
@property
1723
def status_code(self) -> int:

Diff for: openapi_core/protocols.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ class Response(Protocol):
120120
"""
121121

122122
@property
123-
def data(self) -> str:
123+
def data(self) -> Optional[bytes]:
124124
...
125125

126126
@property

Diff for: openapi_core/testing/responses.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
class MockResponse:
1010
def __init__(
1111
self,
12-
data: str,
12+
data: bytes,
1313
status_code: int = 200,
1414
headers: Optional[Dict[str, Any]] = None,
1515
content_type: str = "application/json",

Diff for: openapi_core/validation/response/validators.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing import Iterator
66
from typing import List
77
from typing import Mapping
8+
from typing import Optional
89

910
from jsonschema_path import SchemaPath
1011
from openapi_spec_validator import OpenAPIV30SpecValidator
@@ -41,7 +42,7 @@ class BaseResponseValidator(BaseValidator):
4142
def _iter_errors(
4243
self,
4344
status_code: int,
44-
data: str,
45+
data: Optional[bytes],
4546
headers: Mapping[str, Any],
4647
mimetype: str,
4748
operation: SchemaPath,
@@ -66,7 +67,11 @@ def _iter_errors(
6667
yield from exc.context
6768

6869
def _iter_data_errors(
69-
self, status_code: int, data: str, mimetype: str, operation: SchemaPath
70+
self,
71+
status_code: int,
72+
data: Optional[bytes],
73+
mimetype: str,
74+
operation: SchemaPath,
7075
) -> Iterator[Exception]:
7176
try:
7277
operation_response = self._find_operation_response(
@@ -114,7 +119,10 @@ def _find_operation_response(
114119

115120
@ValidationErrorWrapper(DataValidationError, InvalidData)
116121
def _get_data(
117-
self, data: str, mimetype: str, operation_response: SchemaPath
122+
self,
123+
data: Optional[bytes],
124+
mimetype: str,
125+
operation_response: SchemaPath,
118126
) -> Any:
119127
if "content" not in operation_response:
120128
return None
@@ -125,7 +133,7 @@ def _get_data(
125133
value, _ = self._get_content_and_schema(raw_data, content, mimetype)
126134
return value
127135

128-
def _get_data_value(self, data: str) -> Any:
136+
def _get_data_value(self, data: Optional[bytes]) -> bytes:
129137
if not data:
130138
raise MissingData
131139

Diff for: tests/integration/contrib/aiohttp/test_aiohttp_project.py

-4
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,6 @@ def api_key_encoded(self):
3838

3939

4040
class TestPetPhotoView(BaseTestPetstore):
41-
@pytest.mark.xfail(
42-
reason="response binary format not supported",
43-
strict=True,
44-
)
4541
async def test_get_valid(self, client, data_gif):
4642
headers = {
4743
"Authorization": "Basic testuser",

Diff for: tests/integration/contrib/django/test_django_project.py

-4
Original file line numberDiff line numberDiff line change
@@ -398,10 +398,6 @@ def test_get_skip_response_validation(self, client):
398398

399399

400400
class TestPetPhotoView(BaseTestDjangoProject):
401-
@pytest.mark.xfail(
402-
reason="response binary format not supported",
403-
strict=True,
404-
)
405401
def test_get_valid(self, client, data_gif):
406402
headers = {
407403
"HTTP_AUTHORIZATION": "Basic testuser",

Diff for: tests/integration/contrib/falcon/test_falcon_project.py

-4
Original file line numberDiff line numberDiff line change
@@ -371,10 +371,6 @@ def test_delete_method_invalid(self, client):
371371

372372

373373
class TestPetPhotoResource(BaseTestFalconProject):
374-
@pytest.mark.xfail(
375-
reason="response binary format not supported",
376-
strict=True,
377-
)
378374
def test_get_valid(self, client, data_gif):
379375
cookies = {"user": 1}
380376
headers = {

Diff for: tests/integration/contrib/flask/test_flask_project.py

-4
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,6 @@ def api_key_encoded(self):
4141

4242

4343
class TestPetPhotoView(BaseTestFlaskProject):
44-
@pytest.mark.xfail(
45-
reason="response binary format not supported",
46-
strict=True,
47-
)
4844
def test_get_valid(self, client, data_gif):
4945
headers = {
5046
"Authorization": "Basic testuser",

Diff for: tests/integration/contrib/requests/test_requests_validation.py

-4
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,6 @@ def request_unmarshaller(self, spec):
165165
def response_unmarshaller(self, spec):
166166
return V30ResponseUnmarshaller(spec)
167167

168-
@pytest.mark.xfail(
169-
reason="response binary format not supported",
170-
strict=True,
171-
)
172168
@responses.activate
173169
def test_response_binary_valid(self, response_unmarshaller, data_gif):
174170
responses.add(

Diff for: tests/integration/contrib/starlette/data/v3.0/starletteproject/pets/endpoints.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@ def pet_photo_endpoint(request):
2424
openapi_request = StarletteOpenAPIRequest(request)
2525
request_unmarshalled = unmarshal_request(openapi_request, spec=spec)
2626
if request.method == "GET":
27-
response = StreamingResponse([OPENID_LOGO], media_type="image/gif")
27+
contents = iter([OPENID_LOGO])
28+
response = StreamingResponse(contents, media_type="image/gif")
29+
openapi_response = StarletteOpenAPIResponse(response, data=OPENID_LOGO)
2830
elif request.method == "POST":
2931
contents = request.body()
3032
response = Response(status_code=201)
31-
openapi_response = StarletteOpenAPIResponse(response)
33+
openapi_response = StarletteOpenAPIResponse(response)
3234
response_unmarshalled = unmarshal_response(
3335
openapi_request, openapi_response, spec=spec
3436
)

Diff for: tests/integration/contrib/starlette/test_starlette_project.py

+1-5
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,6 @@ def api_key_encoded(self):
3838

3939

4040
class TestPetPhotoView(BaseTestPetstore):
41-
@pytest.mark.xfail(
42-
reason="response binary format not supported",
43-
strict=True,
44-
)
4541
def test_get_valid(self, client, data_gif):
4642
headers = {
4743
"Authorization": "Basic testuser",
@@ -55,7 +51,7 @@ def test_get_valid(self, client, data_gif):
5551
cookies=cookies,
5652
)
5753

58-
assert response.get_data() == data_gif
54+
assert response.content == data_gif
5955
assert response.status_code == 200
6056

6157
def test_post_valid(self, client, data_gif):

0 commit comments

Comments
 (0)