diff --git a/.readthedocs.yaml b/.readthedocs.yaml index c50b80a8..109375e3 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -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: diff --git a/aiopenapi3/model.py b/aiopenapi3/model.py index 063666dc..dd6c8531 100644 --- a/aiopenapi3/model.py +++ b/aiopenapi3/model.py @@ -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 @@ -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) diff --git a/aiopenapi3/v20/glue.py b/aiopenapi3/v20/glue.py index e6abe998..c6d8a4c1 100644 --- a/aiopenapi3/v20/glue.py +++ b/aiopenapi3/v20/glue.py @@ -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) @@ -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) @@ -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) @@ -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): diff --git a/tests/fixtures/paths-parameter-format-v20.yaml b/tests/fixtures/paths-parameter-format-v20.yaml index 83e29066..3d10ee35 100644 --- a/tests/fixtures/paths-parameter-format-v20.yaml +++ b/tests/fixtures/paths-parameter-format-v20.yaml @@ -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 diff --git a/tests/pathv20_test.py b/tests/pathv20_test.py index e2868cbc..162577d7 100644 --- a/tests/pathv20_test.py +++ b/tests/pathv20_test.py @@ -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)