Skip to content

Commit 756f34a

Browse files
committed
v20 - schema with type file is valid in Response to indicate binary content
c.f. h44z/wg-portal#561
1 parent 8c044c5 commit 756f34a

File tree

5 files changed

+52
-13
lines changed

5 files changed

+52
-13
lines changed

.readthedocs.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ version: 2
77

88
# Set the version of Python and other tools you might need
99
build:
10-
os: ubuntu-20.04
10+
os: ubuntu-22.04
1111
tools:
12-
python: "3.9"
12+
python: "3.10"
1313
apt_packages:
1414
- graphviz
1515
jobs:

aiopenapi3/model.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ def generate_type_format_to_class():
5757

5858
type_format_to_class["integer"][None] = int
5959

60+
assert type_format_to_class["string"]["binary"] == bytes
61+
6062
try:
6163
from pydantic_extra_types import epoch
6264

@@ -439,6 +441,18 @@ def validate_patternProperties(self_):
439441
for i in schema.allOf:
440442
classinfo.createAnnotations(i, discriminators, schemanames, fwdref=True)
441443
classinfo.createFields(i)
444+
elif _type == "file":
445+
"""
446+
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.
447+
448+
https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#data-types
449+
450+
type:string format:binary is bytes in pydantic validation
451+
"""
452+
assert isinstance(schema, v20.Schema)
453+
schema_ = v20.Schema(type="string", format="binary")
454+
_t = Model.createAnnotation(schema_, _type="string")
455+
classinfo.root = Annotated[_t, Model.createField(schema_, _type="string", args=None)]
442456
else:
443457
raise ValueError(_type)
444458

aiopenapi3/v20/glue.py

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ def _process_stream(self, result: httpx.Response) -> tuple["ResponseHeadersType"
291291
headers = self._process__headers(result, result.headers, expected_response)
292292
return headers, expected_response.schema_
293293

294-
def _process_request(self, result: httpx.Response) -> tuple["ResponseHeadersType", "ResponseDataType"]:
294+
def _process_request(self, result: httpx.Response) -> tuple["ResponseHeadersType", Optional["ResponseDataType"]]:
295295
rheaders: "ResponseHeadersType"
296296
# spec enforces these are strings
297297
status_code = str(result.status_code)
@@ -306,7 +306,7 @@ def _process_request(self, result: httpx.Response) -> tuple["ResponseHeadersType
306306
content_type=content_type,
307307
)
308308
status_code = ctx.status_code
309-
content_type = ctx.content_type
309+
content_type = ctx.content_type.lower().partition(";")[0] if ctx.content_type is not None else None
310310
headers = ctx.headers
311311

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

323-
if content_type and content_type.lower().partition(";")[0] == "application/json":
323+
if content_type not in (produces := (self.operation.produces or self.api._root.produces)):
324+
raise ContentTypeError(
325+
self.operation,
326+
content_type,
327+
f"Unexpected Content-Type {content_type} returned for operation {self.operation.operationId} (expected {produces})",
328+
result,
329+
)
330+
331+
if content_type == "application/json":
324332
data = ctx.received.decode()
325333
try:
326334
data = json.loads(data)
@@ -349,16 +357,15 @@ def _process_request(self, result: httpx.Response) -> tuple["ResponseHeadersType
349357
self._raise_on_http_status(int(status_code), rheaders, data)
350358

351359
return rheaders, data
352-
elif self.operation.produces and content_type in self.operation.produces:
360+
else:
361+
"""
362+
We have received a valid (i.e. expected) content type,
363+
e.g. application/octet-stream
364+
but we can't validate it since it's not json.
365+
"""
366+
353367
self._raise_on_http_status(result.status_code, rheaders, ctx.received)
354368
return rheaders, ctx.received
355-
else:
356-
raise ContentTypeError(
357-
self.operation,
358-
content_type,
359-
f"Unexpected Content-Type {content_type} returned for operation {self.operation.operationId} (expected application/json)",
360-
result,
361-
)
362369

363370

364371
class AsyncRequest(Request, AsyncRequestBase):

tests/fixtures/paths-parameter-format-v20.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,14 @@ paths:
111111
description: OK
112112
schema:
113113
type: string
114+
115+
'/file':
116+
get:
117+
operationId: getfile
118+
produces:
119+
- application/octet-stream
120+
responses:
121+
"200":
122+
description: OK
123+
schema:
124+
type: file

tests/pathv20_test.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,13 @@ def on_file(file):
212212
return
213213

214214

215+
def test_paths_response_file(httpx_mock, with_paths_parameter_format_v20):
216+
httpx_mock.add_response(headers={"Content-Type": "application/octet-stream"}, content=b"\x00")
217+
api = OpenAPI(URLBASE, with_paths_parameter_format_v20, session_factory=httpx.Client)
218+
f = api._.getfile()
219+
assert f == b"\x00"
220+
221+
215222
def test_paths_stream(httpx_mock, with_paths_parameter_format_v20):
216223
httpx_mock.add_response(headers={"Content-Type": "application/json"}, json="ok")
217224
api = OpenAPI(URLBASE, with_paths_parameter_format_v20, session_factory=httpx.Client)

0 commit comments

Comments
 (0)