Skip to content

Commit 09f065b

Browse files
authored
Merge pull request #710 from python-openapi/feature/request-response-binary-format-support
Request response binary format support
2 parents 16e7b1a + 10fdbea commit 09f065b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+239
-221
lines changed

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class Empty:
1919
class AIOHTTPOpenAPIWebRequest:
2020
__slots__ = ("request", "parameters", "_get_body", "_body")
2121

22-
def __init__(self, request: web.Request, *, body: str | None):
22+
def __init__(self, request: web.Request, *, body: bytes | None):
2323
if not isinstance(request, web.Request):
2424
raise TypeError(
2525
f"'request' argument is not type of {web.Request.__qualname__!r}"
@@ -45,7 +45,7 @@ def method(self) -> str:
4545
return self.request.method.lower()
4646

4747
@property
48-
def body(self) -> str | None:
48+
def body(self) -> bytes | None:
4949
return self._body
5050

5151
@property

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/requests.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,9 @@ def method(self) -> str:
7676
return self.request.method.lower()
7777

7878
@property
79-
def body(self) -> str:
79+
def body(self) -> bytes:
8080
assert isinstance(self.request.body, bytes)
81-
return self.request.body.decode("utf-8")
81+
return self.request.body
8282

8383
@property
8484
def content_type(self) -> str:

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/requests.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,14 @@ def method(self) -> str:
4949
return self.request.method.lower()
5050

5151
@property
52-
def body(self) -> Optional[str]:
52+
def body(self) -> Optional[bytes]:
53+
# Falcon doesn't store raw request stream.
54+
# That's why we need to revert deserialized data
55+
5356
# Support falcon-jsonify.
5457
if hasattr(self.request, "json"):
55-
return dumps(self.request.json)
58+
return dumps(self.request.json).encode("utf-8")
5659

57-
# Falcon doesn't store raw request stream.
58-
# That's why we need to revert serialized data
5960
media = self.request.get_media(
6061
default_when_empty=self.default_when_empty,
6162
)
@@ -74,7 +75,7 @@ def body(self) -> Optional[str]:
7475
return None
7576
else:
7677
assert isinstance(body, bytes)
77-
return body.decode("utf-8")
78+
return body
7879

7980
@property
8081
def content_type(self) -> str:

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/requests.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,14 @@ def method(self) -> str:
6464
return method and method.lower() or ""
6565

6666
@property
67-
def body(self) -> Optional[str]:
67+
def body(self) -> Optional[bytes]:
6868
if self.request.body is None:
6969
return None
7070
if isinstance(self.request.body, bytes):
71-
return self.request.body.decode("utf-8")
71+
return self.request.body
7272
assert isinstance(self.request.body, str)
7373
# TODO: figure out if request._body_position is relevant
74-
return self.request.body
74+
return self.request.body.encode("utf-8")
7575

7676
@property
7777
def content_type(self) -> str:

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/requests.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,14 @@ def method(self) -> str:
3434
return self.request.method.lower()
3535

3636
@property
37-
def body(self) -> Optional[str]:
37+
def body(self) -> Optional[bytes]:
3838
body = self._get_body()
3939
if body is None:
4040
return None
4141
if isinstance(body, bytes):
42-
return body.decode("utf-8")
42+
return body
4343
assert isinstance(body, str)
44-
return body
44+
return body.encode("utf-8")
4545

4646
@property
4747
def content_type(self) -> str:

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/requests.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ def method(self) -> str:
3939
return self.request.method.lower()
4040

4141
@property
42-
def body(self) -> Optional[str]:
43-
return self.request.get_data(as_text=True)
42+
def body(self) -> Optional[bytes]:
43+
return self.request.get_data(as_text=False)
4444

4545
@property
4646
def content_type(self) -> str:

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/deserializing/media_types/datatypes.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
from typing import Callable
33
from typing import Dict
44

5-
DeserializerCallable = Callable[[Any], Any]
5+
DeserializerCallable = Callable[[bytes], Any]
66
MediaTypeDeserializersDict = Dict[str, DeserializerCallable]

Diff for: openapi_core/deserializing/media_types/deserializers.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ def __init__(
4141
extra_media_type_deserializers = {}
4242
self.extra_media_type_deserializers = extra_media_type_deserializers
4343

44-
def deserialize(self, mimetype: str, value: Any, **parameters: str) -> Any:
44+
def deserialize(
45+
self, mimetype: str, value: bytes, **parameters: str
46+
) -> Any:
4547
deserializer_callable = self.get_deserializer_callable(mimetype)
4648

4749
try:
@@ -75,7 +77,7 @@ def __init__(
7577
self.encoding = encoding
7678
self.parameters = parameters
7779

78-
def deserialize(self, value: Any) -> Any:
80+
def deserialize(self, value: bytes) -> Any:
7981
deserialized = self.media_types_deserializer.deserialize(
8082
self.mimetype, value, **self.parameters
8183
)
@@ -192,5 +194,4 @@ def decode_property_content_type(
192194
value = location.getlist(prop_name)
193195
return list(map(prop_deserializer.deserialize, value))
194196

195-
value = location[prop_name]
196-
return prop_deserializer.deserialize(value)
197+
return prop_deserializer.deserialize(location[prop_name])

Diff for: openapi_core/deserializing/media_types/exceptions.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ class MediaTypeDeserializeError(DeserializeError):
88
"""Media type deserialize operation error"""
99

1010
mimetype: str
11-
value: str
11+
value: bytes
1212

1313
def __str__(self) -> str:
1414
return (
1515
"Failed to deserialize value with {mimetype} mimetype: {value}"
16-
).format(value=self.value, mimetype=self.mimetype)
16+
).format(value=self.value.decode("utf-8"), mimetype=self.mimetype)

Diff for: openapi_core/deserializing/media_types/util.py

+19-18
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,11 @@
1010
from werkzeug.datastructures import ImmutableMultiDict
1111

1212

13-
def binary_loads(value: Union[str, bytes], **parameters: str) -> bytes:
14-
charset = "utf-8"
15-
if "charset" in parameters:
16-
charset = parameters["charset"]
17-
if isinstance(value, str):
18-
return value.encode(charset)
13+
def binary_loads(value: bytes, **parameters: str) -> bytes:
1914
return value
2015

2116

22-
def plain_loads(value: Union[str, bytes], **parameters: str) -> str:
17+
def plain_loads(value: bytes, **parameters: str) -> str:
2318
charset = "utf-8"
2419
if "charset" in parameters:
2520
charset = parameters["charset"]
@@ -32,30 +27,36 @@ def plain_loads(value: Union[str, bytes], **parameters: str) -> str:
3227
return value
3328

3429

35-
def json_loads(value: Union[str, bytes], **parameters: str) -> Any:
30+
def json_loads(value: bytes, **parameters: str) -> Any:
3631
return loads(value)
3732

3833

39-
def xml_loads(value: Union[str, bytes], **parameters: str) -> Element:
40-
return fromstring(value)
34+
def xml_loads(value: bytes, **parameters: str) -> Element:
35+
charset = "utf-8"
36+
if "charset" in parameters:
37+
charset = parameters["charset"]
38+
return fromstring(value.decode(charset))
4139

4240

43-
def urlencoded_form_loads(value: Any, **parameters: str) -> Mapping[str, Any]:
44-
return ImmutableMultiDict(parse_qsl(value))
41+
def urlencoded_form_loads(
42+
value: bytes, **parameters: str
43+
) -> Mapping[str, Any]:
44+
# only UTF-8 is conforming
45+
return ImmutableMultiDict(parse_qsl(value.decode("utf-8")))
4546

4647

47-
def data_form_loads(
48-
value: Union[str, bytes], **parameters: str
49-
) -> Mapping[str, Any]:
50-
if isinstance(value, bytes):
51-
value = value.decode("ASCII", errors="surrogateescape")
48+
def data_form_loads(value: bytes, **parameters: str) -> Mapping[str, Any]:
49+
charset = "ASCII"
50+
if "charset" in parameters:
51+
charset = parameters["charset"]
52+
decoded = value.decode(charset, errors="surrogateescape")
5253
boundary = ""
5354
if "boundary" in parameters:
5455
boundary = parameters["boundary"]
5556
parser = Parser()
5657
mimetype = "multipart/form-data"
5758
header = f'Content-Type: {mimetype}; boundary="{boundary}"'
58-
text = "\n\n".join([header, value])
59+
text = "\n\n".join([header, decoded])
5960
parts = parser.parsestr(text, headersonly=False)
6061
return ImmutableMultiDict(
6162
[

Diff for: openapi_core/protocols.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def method(self) -> str:
1717
...
1818

1919
@property
20-
def body(self) -> Optional[str]:
20+
def body(self) -> Optional[bytes]:
2121
...
2222

2323
@property
@@ -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/requests.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def __init__(
2020
view_args: Optional[Dict[str, Any]] = None,
2121
headers: Optional[Dict[str, Any]] = None,
2222
cookies: Optional[Dict[str, Any]] = None,
23-
data: Optional[str] = None,
23+
data: Optional[bytes] = None,
2424
content_type: str = "application/json",
2525
):
2626
self.host_url = host_url

0 commit comments

Comments
 (0)