Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .readthedocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ version: 2

# Set the version of Python and other tools you might need
build:
os: ubuntu-20.04
os: ubuntu-22.04
tools:
python: "3.9"
python: "3.10"
apt_packages:
- graphviz
jobs:
Expand Down
14 changes: 14 additions & 0 deletions aiopenapi3/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ def generate_type_format_to_class():

type_format_to_class["integer"][None] = int

assert type_format_to_class["string"]["binary"] == bytes

try:
from pydantic_extra_types import epoch

Expand Down Expand Up @@ -439,6 +441,18 @@ def validate_patternProperties(self_):
for i in schema.allOf:
classinfo.createAnnotations(i, discriminators, schemanames, fwdref=True)
classinfo.createFields(i)
elif _type == "file":
"""
An additional primitive data type "file" is used by the Parameter Object and the Response Object to set the parameter type or the response as being a file.

https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#data-types

type:string format:binary is bytes in pydantic validation
"""
assert isinstance(schema, v20.Schema)
schema_ = v20.Schema(type="string", format="binary")
_t = Model.createAnnotation(schema_, _type="string")
classinfo.root = Annotated[_t, Model.createField(schema_, _type="string", args=None)]
else:
raise ValueError(_type)

Expand Down
29 changes: 18 additions & 11 deletions aiopenapi3/v20/glue.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ def _process_stream(self, result: httpx.Response) -> tuple["ResponseHeadersType"
headers = self._process__headers(result, result.headers, expected_response)
return headers, expected_response.schema_

def _process_request(self, result: httpx.Response) -> tuple["ResponseHeadersType", "ResponseDataType"]:
def _process_request(self, result: httpx.Response) -> tuple["ResponseHeadersType", Optional["ResponseDataType"]]:
rheaders: "ResponseHeadersType"
# spec enforces these are strings
status_code = str(result.status_code)
Expand All @@ -306,7 +306,7 @@ def _process_request(self, result: httpx.Response) -> tuple["ResponseHeadersType
content_type=content_type,
)
status_code = ctx.status_code
content_type = ctx.content_type
content_type = ctx.content_type.lower().partition(";")[0] if ctx.content_type is not None else None
headers = ctx.headers

expected_response = self._process__status_code(result, status_code)
Expand All @@ -320,7 +320,15 @@ def _process_request(self, result: httpx.Response) -> tuple["ResponseHeadersType
if status_code == "204":
return rheaders, None

if content_type and content_type.lower().partition(";")[0] == "application/json":
if content_type not in (produces := (self.operation.produces or self.api._root.produces)):
raise ContentTypeError(
self.operation,
content_type,
f"Unexpected Content-Type {content_type} returned for operation {self.operation.operationId} (expected {produces})",
result,
)

if content_type == "application/json":
data = ctx.received.decode()
try:
data = json.loads(data)
Expand Down Expand Up @@ -349,16 +357,15 @@ def _process_request(self, result: httpx.Response) -> tuple["ResponseHeadersType
self._raise_on_http_status(int(status_code), rheaders, data)

return rheaders, data
elif self.operation.produces and content_type in self.operation.produces:
else:
"""
We have received a valid (i.e. expected) content type,
e.g. application/octet-stream
but we can't validate it since it's not json.
"""

self._raise_on_http_status(result.status_code, rheaders, ctx.received)
return rheaders, ctx.received
else:
raise ContentTypeError(
self.operation,
content_type,
f"Unexpected Content-Type {content_type} returned for operation {self.operation.operationId} (expected application/json)",
result,
)


class AsyncRequest(Request, AsyncRequestBase):
Expand Down
11 changes: 11 additions & 0 deletions tests/fixtures/paths-parameter-format-v20.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,14 @@ paths:
description: OK
schema:
type: string

'/file':
get:
operationId: getfile
produces:
- application/octet-stream
responses:
"200":
description: OK
schema:
type: file
7 changes: 7 additions & 0 deletions tests/pathv20_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,13 @@ def on_file(file):
return


def test_paths_response_file(httpx_mock, with_paths_parameter_format_v20):
httpx_mock.add_response(headers={"Content-Type": "application/octet-stream"}, content=b"\x00")
api = OpenAPI(URLBASE, with_paths_parameter_format_v20, session_factory=httpx.Client)
f = api._.getfile()
assert f == b"\x00"


def test_paths_stream(httpx_mock, with_paths_parameter_format_v20):
httpx_mock.add_response(headers={"Content-Type": "application/json"}, json="ok")
api = OpenAPI(URLBASE, with_paths_parameter_format_v20, session_factory=httpx.Client)
Expand Down