Skip to content

Commit 0031ff7

Browse files
committed
Request binary format support
1 parent 16e7b1a commit 0031ff7

File tree

28 files changed

+130
-130
lines changed

28 files changed

+130
-130
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/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/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/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/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/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/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

+1-1
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

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

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ def _get_security_value(
248248

249249
@ValidationErrorWrapper(RequestBodyValidationError, InvalidRequestBody)
250250
def _get_body(
251-
self, body: Optional[str], mimetype: str, operation: SchemaPath
251+
self, body: Optional[bytes], mimetype: str, operation: SchemaPath
252252
) -> Any:
253253
if "requestBody" not in operation:
254254
return None
@@ -262,8 +262,8 @@ def _get_body(
262262
return value
263263

264264
def _get_body_value(
265-
self, body: Optional[str], request_body: SchemaPath
266-
) -> Any:
265+
self, body: Optional[bytes], request_body: SchemaPath
266+
) -> bytes:
267267
if not body:
268268
if request_body.getkey("required", False):
269269
raise MissingRequiredRequestBody

Diff for: openapi_core/validation/validators.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def _deserialise_media_type(
106106
media_type: SchemaPath,
107107
mimetype: str,
108108
parameters: Mapping[str, str],
109-
value: Any,
109+
value: bytes,
110110
) -> Any:
111111
schema = media_type.get("schema")
112112
encoding = None
@@ -222,7 +222,7 @@ def _get_complex_param_or_header(
222222

223223
def _get_content_schema_value_and_schema(
224224
self,
225-
raw: Any,
225+
raw: bytes,
226226
content: SchemaPath,
227227
mimetype: Optional[str] = None,
228228
) -> Tuple[Any, Optional[SchemaPath]]:
@@ -246,7 +246,7 @@ def _get_content_schema_value_and_schema(
246246
return casted, schema
247247

248248
def _get_content_and_schema(
249-
self, raw: Any, content: SchemaPath, mimetype: Optional[str] = None
249+
self, raw: bytes, content: SchemaPath, mimetype: Optional[str] = None
250250
) -> Tuple[Any, Optional[SchemaPath]]:
251251
casted, schema = self._get_content_schema_value_and_schema(
252252
raw, content, mimetype

Diff for: tests/integration/contrib/django/data/v3.0/djangoproject/pets/views.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ def get(self, request, petId):
110110
)
111111
return django_response
112112

113-
def post(self, request):
113+
def post(self, request, petId):
114114
assert request.openapi
115115
assert not request.openapi.errors
116116

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

+1-5
Original file line numberDiff line numberDiff line change
@@ -412,10 +412,6 @@ def test_get_valid(self, client, data_gif):
412412
assert response.status_code == 200
413413
assert b"".join(list(response.streaming_content)) == data_gif
414414

415-
@pytest.mark.xfail(
416-
reason="request binary format not supported",
417-
strict=True,
418-
)
419415
def test_post_valid(self, client, data_gif):
420416
client.cookies.load({"user": 1})
421417
content_type = "image/gif"
@@ -425,7 +421,7 @@ def test_post_valid(self, client, data_gif):
425421
"HTTP_API_KEY": self.api_key_encoded,
426422
}
427423
response = client.post(
428-
"/v1/pets/12/photo", data_gif, content_type, secure=True, **headers
424+
"/v1/pets/12/photo", data_gif, content_type, **headers
429425
)
430426

431427
assert response.status_code == 201

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

+4-5
Original file line numberDiff line numberDiff line change
@@ -393,24 +393,23 @@ def test_get_valid(self, client, data_gif):
393393
assert response.status_code == 200
394394

395395
@pytest.mark.xfail(
396-
reason="request binary format not supported",
396+
reason="falcon request binary handler not implemented",
397397
strict=True,
398398
)
399-
def test_post_valid(self, client, data_json):
399+
def test_post_valid(self, client, data_gif):
400400
cookies = {"user": 1}
401-
content_type = "image/gif"
401+
content_type = "image/jpeg"
402402
headers = {
403403
"Authorization": "Basic testuser",
404404
"Api-Key": self.api_key_encoded,
405405
"Content-Type": content_type,
406406
}
407-
body = dumps(data_json)
408407

409408
response = client.simulate_post(
410409
"/v1/pets/1/photo",
411410
host="petstore.swagger.io",
412411
headers=headers,
413-
body=body,
412+
body=data_gif,
414413
cookies=cookies,
415414
)
416415

Diff for: tests/integration/contrib/flask/data/v3.0/flaskproject/pets/views.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from base64 import b64decode
22
from io import BytesIO
33

4+
from flask import Response
5+
from flask import request
46
from flask.helpers import send_file
57

68
from openapi_core.contrib.flask.views import FlaskOpenAPIView
@@ -23,4 +25,4 @@ def get(self, petId):
2325

2426
def post(self, petId):
2527
data = request.stream.read()
26-
response.status = HTTP_201
28+
return Response(status=201)

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

+3-10
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def test_get_valid(self, client, data_gif):
5151
"Api-Key": self.api_key_encoded,
5252
}
5353

54-
client.set_cookie("petstore.swagger.io", "user", "1")
54+
client.set_cookie("user", "1", domain="petstore.swagger.io")
5555
response = client.get(
5656
"/v1/pets/1/photo",
5757
headers=headers,
@@ -60,26 +60,19 @@ def test_get_valid(self, client, data_gif):
6060
assert response.get_data() == data_gif
6161
assert response.status_code == 200
6262

63-
@pytest.mark.xfail(
64-
reason="request binary format not supported",
65-
strict=True,
66-
)
6763
def test_post_valid(self, client, data_gif):
6864
content_type = "image/gif"
6965
headers = {
7066
"Authorization": "Basic testuser",
7167
"Api-Key": self.api_key_encoded,
7268
"Content-Type": content_type,
7369
}
74-
data = {
75-
"file": data_gif,
76-
}
7770

78-
client.set_cookie("petstore.swagger.io", "user", "1")
71+
client.set_cookie("user", "1", domain="petstore.swagger.io")
7972
response = client.post(
8073
"/v1/pets/1/photo",
8174
headers=headers,
82-
data=data,
75+
data=data_gif,
8376
)
8477

8578
assert not response.text

0 commit comments

Comments
 (0)