Skip to content

Commit

Permalink
Merge pull request #710 from python-openapi/feature/request-response-…
Browse files Browse the repository at this point in the history
…binary-format-support

Request response binary format support
  • Loading branch information
p1c2u authored Nov 1, 2023
2 parents 16e7b1a + 10fdbea commit 09f065b
Show file tree
Hide file tree
Showing 43 changed files with 239 additions and 221 deletions.
4 changes: 2 additions & 2 deletions openapi_core/contrib/aiohttp/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class Empty:
class AIOHTTPOpenAPIWebRequest:
__slots__ = ("request", "parameters", "_get_body", "_body")

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

@property
def body(self) -> str | None:
def body(self) -> bytes | None:
return self._body

@property
Expand Down
8 changes: 4 additions & 4 deletions openapi_core/contrib/aiohttp/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ def __init__(self, response: web.Response):
self.response = response

@property
def data(self) -> str:
def data(self) -> bytes:
if self.response.body is None:
return ""
return b""
if isinstance(self.response.body, bytes):
return self.response.body.decode("utf-8")
return self.response.body
assert isinstance(self.response.body, str)
return self.response.body
return self.response.body.encode("utf-8")

@property
def status_code(self) -> int:
Expand Down
4 changes: 2 additions & 2 deletions openapi_core/contrib/django/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@ def method(self) -> str:
return self.request.method.lower()

@property
def body(self) -> str:
def body(self) -> bytes:
assert isinstance(self.request.body, bytes)
return self.request.body.decode("utf-8")
return self.request.body

@property
def content_type(self) -> str:
Expand Down
16 changes: 12 additions & 4 deletions openapi_core/contrib/django/responses.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
"""OpenAPI core contrib django responses module"""
from itertools import tee

from django.http.response import HttpResponse
from django.http.response import StreamingHttpResponse
from werkzeug.datastructures import Headers


class DjangoOpenAPIResponse:
def __init__(self, response: HttpResponse):
if not isinstance(response, HttpResponse):
if not isinstance(response, (HttpResponse, StreamingHttpResponse)):
raise TypeError(
f"'response' argument is not type of {HttpResponse}"
f"'response' argument is not type of {HttpResponse} or {StreamingHttpResponse}"
)
self.response = response

@property
def data(self) -> str:
def data(self) -> bytes:
if isinstance(self.response, StreamingHttpResponse):
resp_iter1, resp_iter2 = tee(self.response._iterator)
self.response.streaming_content = resp_iter1
content = b"".join(map(self.response.make_bytes, resp_iter2))
return content
assert isinstance(self.response.content, bytes)
return self.response.content.decode("utf-8")
return self.response.content

@property
def status_code(self) -> int:
Expand Down
11 changes: 6 additions & 5 deletions openapi_core/contrib/falcon/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,14 @@ def method(self) -> str:
return self.request.method.lower()

@property
def body(self) -> Optional[str]:
def body(self) -> Optional[bytes]:
# Falcon doesn't store raw request stream.
# That's why we need to revert deserialized data

# Support falcon-jsonify.
if hasattr(self.request, "json"):
return dumps(self.request.json)
return dumps(self.request.json).encode("utf-8")

# Falcon doesn't store raw request stream.
# That's why we need to revert serialized data
media = self.request.get_media(
default_when_empty=self.default_when_empty,
)
Expand All @@ -74,7 +75,7 @@ def body(self) -> Optional[str]:
return None
else:
assert isinstance(body, bytes)
return body.decode("utf-8")
return body

@property
def content_type(self) -> str:
Expand Down
13 changes: 10 additions & 3 deletions openapi_core/contrib/falcon/responses.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""OpenAPI core contrib falcon responses module"""
from itertools import tee

from falcon.response import Response
from werkzeug.datastructures import Headers

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

@property
def data(self) -> str:
def data(self) -> bytes:
if self.response.text is None:
return ""
if self.response.stream is None:
return b""
resp_iter1, resp_iter2 = tee(self.response.stream)
self.response.stream = resp_iter1
content = b"".join(resp_iter2)
return content
assert isinstance(self.response.text, str)
return self.response.text
return self.response.text.encode("utf-8")

@property
def status_code(self) -> int:
Expand Down
6 changes: 3 additions & 3 deletions openapi_core/contrib/requests/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,14 @@ def method(self) -> str:
return method and method.lower() or ""

@property
def body(self) -> Optional[str]:
def body(self) -> Optional[bytes]:
if self.request.body is None:
return None
if isinstance(self.request.body, bytes):
return self.request.body.decode("utf-8")
return self.request.body
assert isinstance(self.request.body, str)
# TODO: figure out if request._body_position is relevant
return self.request.body
return self.request.body.encode("utf-8")

@property
def content_type(self) -> str:
Expand Down
4 changes: 2 additions & 2 deletions openapi_core/contrib/requests/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ def __init__(self, response: Response):
self.response = response

@property
def data(self) -> str:
def data(self) -> bytes:
assert isinstance(self.response.content, bytes)
return self.response.content.decode("utf-8")
return self.response.content

@property
def status_code(self) -> int:
Expand Down
6 changes: 3 additions & 3 deletions openapi_core/contrib/starlette/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,14 @@ def method(self) -> str:
return self.request.method.lower()

@property
def body(self) -> Optional[str]:
def body(self) -> Optional[bytes]:
body = self._get_body()
if body is None:
return None
if isinstance(body, bytes):
return body.decode("utf-8")
return body
assert isinstance(body, str)
return body
return body.encode("utf-8")

@property
def content_type(self) -> str:
Expand Down
19 changes: 15 additions & 4 deletions openapi_core/contrib/starlette/responses.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
"""OpenAPI core contrib starlette responses module"""
from typing import Optional

from starlette.datastructures import Headers
from starlette.responses import Response
from starlette.responses import StreamingResponse


class StarletteOpenAPIResponse:
def __init__(self, response: Response):
def __init__(self, response: Response, data: Optional[bytes] = None):
if not isinstance(response, Response):
raise TypeError(f"'response' argument is not type of {Response}")
self.response = response

if data is None and isinstance(response, StreamingResponse):
raise RuntimeError(
f"'data' argument is required for {StreamingResponse}"
)
self._data = data

@property
def data(self) -> str:
def data(self) -> bytes:
if self._data is not None:
return self._data
if isinstance(self.response.body, bytes):
return self.response.body.decode("utf-8")
return self.response.body
assert isinstance(self.response.body, str)
return self.response.body
return self.response.body.encode("utf-8")

@property
def status_code(self) -> int:
Expand Down
4 changes: 2 additions & 2 deletions openapi_core/contrib/werkzeug/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ def method(self) -> str:
return self.request.method.lower()

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

@property
def content_type(self) -> str:
Expand Down
10 changes: 8 additions & 2 deletions openapi_core/contrib/werkzeug/responses.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""OpenAPI core contrib werkzeug responses module"""
from itertools import tee

from werkzeug.datastructures import Headers
from werkzeug.wrappers import Response

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

@property
def data(self) -> str:
return self.response.get_data(as_text=True)
def data(self) -> bytes:
if not self.response.is_sequence:
resp_iter1, resp_iter2 = tee(self.response.iter_encoded())
self.response.response = resp_iter1
return b"".join(resp_iter2)
return self.response.get_data(as_text=False)

@property
def status_code(self) -> int:
Expand Down
2 changes: 1 addition & 1 deletion openapi_core/deserializing/media_types/datatypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
from typing import Callable
from typing import Dict

DeserializerCallable = Callable[[Any], Any]
DeserializerCallable = Callable[[bytes], Any]
MediaTypeDeserializersDict = Dict[str, DeserializerCallable]
9 changes: 5 additions & 4 deletions openapi_core/deserializing/media_types/deserializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ def __init__(
extra_media_type_deserializers = {}
self.extra_media_type_deserializers = extra_media_type_deserializers

def deserialize(self, mimetype: str, value: Any, **parameters: str) -> Any:
def deserialize(
self, mimetype: str, value: bytes, **parameters: str
) -> Any:
deserializer_callable = self.get_deserializer_callable(mimetype)

try:
Expand Down Expand Up @@ -75,7 +77,7 @@ def __init__(
self.encoding = encoding
self.parameters = parameters

def deserialize(self, value: Any) -> Any:
def deserialize(self, value: bytes) -> Any:
deserialized = self.media_types_deserializer.deserialize(
self.mimetype, value, **self.parameters
)
Expand Down Expand Up @@ -192,5 +194,4 @@ def decode_property_content_type(
value = location.getlist(prop_name)
return list(map(prop_deserializer.deserialize, value))

value = location[prop_name]
return prop_deserializer.deserialize(value)
return prop_deserializer.deserialize(location[prop_name])
4 changes: 2 additions & 2 deletions openapi_core/deserializing/media_types/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ class MediaTypeDeserializeError(DeserializeError):
"""Media type deserialize operation error"""

mimetype: str
value: str
value: bytes

def __str__(self) -> str:
return (
"Failed to deserialize value with {mimetype} mimetype: {value}"
).format(value=self.value, mimetype=self.mimetype)
).format(value=self.value.decode("utf-8"), mimetype=self.mimetype)
37 changes: 19 additions & 18 deletions openapi_core/deserializing/media_types/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,11 @@
from werkzeug.datastructures import ImmutableMultiDict


def binary_loads(value: Union[str, bytes], **parameters: str) -> bytes:
charset = "utf-8"
if "charset" in parameters:
charset = parameters["charset"]
if isinstance(value, str):
return value.encode(charset)
def binary_loads(value: bytes, **parameters: str) -> bytes:
return value


def plain_loads(value: Union[str, bytes], **parameters: str) -> str:
def plain_loads(value: bytes, **parameters: str) -> str:
charset = "utf-8"
if "charset" in parameters:
charset = parameters["charset"]
Expand All @@ -32,30 +27,36 @@ def plain_loads(value: Union[str, bytes], **parameters: str) -> str:
return value


def json_loads(value: Union[str, bytes], **parameters: str) -> Any:
def json_loads(value: bytes, **parameters: str) -> Any:
return loads(value)


def xml_loads(value: Union[str, bytes], **parameters: str) -> Element:
return fromstring(value)
def xml_loads(value: bytes, **parameters: str) -> Element:
charset = "utf-8"
if "charset" in parameters:
charset = parameters["charset"]
return fromstring(value.decode(charset))


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


def data_form_loads(
value: Union[str, bytes], **parameters: str
) -> Mapping[str, Any]:
if isinstance(value, bytes):
value = value.decode("ASCII", errors="surrogateescape")
def data_form_loads(value: bytes, **parameters: str) -> Mapping[str, Any]:
charset = "ASCII"
if "charset" in parameters:
charset = parameters["charset"]
decoded = value.decode(charset, errors="surrogateescape")
boundary = ""
if "boundary" in parameters:
boundary = parameters["boundary"]
parser = Parser()
mimetype = "multipart/form-data"
header = f'Content-Type: {mimetype}; boundary="{boundary}"'
text = "\n\n".join([header, value])
text = "\n\n".join([header, decoded])
parts = parser.parsestr(text, headersonly=False)
return ImmutableMultiDict(
[
Expand Down
4 changes: 2 additions & 2 deletions openapi_core/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def method(self) -> str:
...

@property
def body(self) -> Optional[str]:
def body(self) -> Optional[bytes]:
...

@property
Expand Down Expand Up @@ -120,7 +120,7 @@ class Response(Protocol):
"""

@property
def data(self) -> str:
def data(self) -> Optional[bytes]:
...

@property
Expand Down
2 changes: 1 addition & 1 deletion openapi_core/testing/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def __init__(
view_args: Optional[Dict[str, Any]] = None,
headers: Optional[Dict[str, Any]] = None,
cookies: Optional[Dict[str, Any]] = None,
data: Optional[str] = None,
data: Optional[bytes] = None,
content_type: str = "application/json",
):
self.host_url = host_url
Expand Down
Loading

0 comments on commit 09f065b

Please sign in to comment.