From a23fd45bcc0864f916db11b866d171886d95c40d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20K=C3=B6tter?= Date: Thu, 3 Sep 2026 08:04:48 +0200 Subject: [PATCH] v3x - allow using Request.sequence for arrays --- docs/source/advanced.rst | 7 +++++-- src/aiopenapi3/request.py | 25 +++++++++++++++++++------ src/aiopenapi3/v30/glue.py | 12 ++++++++---- tests/sequential_test.py | 16 ++++++++++++++++ 4 files changed, 48 insertions(+), 12 deletions(-) diff --git a/docs/source/advanced.rst b/docs/source/advanced.rst index dc301bce..399466c1 100644 --- a/docs/source/advanced.rst +++ b/docs/source/advanced.rst @@ -364,6 +364,7 @@ aiopenapi3 supports sequential media for the following content types: * application/jsonl e.g. '{…}\n' http.Response.iter_lines http.Response.aiter_lines * application/x-ndjson e.g. '{…}\n' http.Response.iter_lines http.Response.aiter_lines * text/event-stream e.g. '[{…},' via ijson + * application/json e.g. '[{…},' via ijson Non-JSON Content @@ -374,9 +375,11 @@ See :aioai3:ref:`tests.stream_test.test_stream_data`. JSON/Arrays of Models ^^^^^^^^^^^^^^^^^^^^^ -In case the large response is an array of models, iterative JSON parsing libraries can be used to process the data. +In case the large response is an array of models, sequence() or stream() can be used. -See :aioai3:ref:`tests.stream_test.test_stream_array`. +See + * :aioai3:ref:`tests.sequential_test.test_array` + * :aioai3:ref:`tests.stream_test.test_stream_array` Session Factory diff --git a/src/aiopenapi3/request.py b/src/aiopenapi3/request.py index 448f1e10..e3353f24 100644 --- a/src/aiopenapi3/request.py +++ b/src/aiopenapi3/request.py @@ -311,13 +311,14 @@ def sequence( # type: ignore[override] self._prepare(data, parameters) session: httpx2.Client = self.api._session_factory(**self._session_factory_default_args) result = self._send(session, data, parameters) - headers, schema_, content_type = self._process_sequence(result) + headers, expected_media, content_type = self._process_sequence(result) if content_type in ["application/jsonl", "application/x-ndjson"]: """ https://jsonlines.org/ https://github.com/ndjson/ndjson-spec """ + schema_ = expected_media.itemSchema def iter_json(response: httpx2.Response) -> Iterator["JSON"]: for i in response.iter_lines(): @@ -328,7 +329,7 @@ def iter_json(response: httpx2.Response) -> Iterator["JSON"]: JSON Text Sequence https://datatracker.ietf.org/doc/html/rfc7464 """ - + schema_ = expected_media.itemSchema import jsonseq.decode def iter_json(response: httpx2.Response) -> Iterator["JSON"]: @@ -341,6 +342,7 @@ def iter_json(response: httpx2.Response) -> Iterator["JSON"]: Server-Sent Events (SSE) https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events """ + schema_ = expected_media.itemSchema def iter_json(response: httpx2.Response) -> Iterator["JSON"]: for chunk in response.iter_text(): @@ -361,7 +363,11 @@ def iter_json(response: httpx2.Response) -> Iterator["JSON"]: v[cmd or "comment"] = value.lstrip() data_ = "" yield v - elif False: + elif content_type == "application/json": + """ + this will iter arrays for OpenAPI 3.x + """ + schema_ = expected_media.schema_.items import ijson class ReadEventStream: @@ -498,13 +504,14 @@ async def sequence( # type: ignore[override] self._prepare(data, parameters) session = self.api._session_factory(**self._session_factory_default_args) result = await self._send(session, data, parameters) - headers, schema_, content_type = self._process_sequence(result) + headers, expected_media, content_type = self._process_sequence(result) if content_type in ["application/jsonl", "application/x-ndjson"]: """ https://jsonlines.org/ https://github.com/ndjson/ndjson-spec """ + schema_ = expected_media.itemSchema async def aiter_json(response: httpx2.Response) -> AsyncIterator["JSON"]: async for i in response.aiter_lines(): @@ -515,7 +522,7 @@ async def aiter_json(response: httpx2.Response) -> AsyncIterator["JSON"]: JSON Text Sequence https://datatracker.ietf.org/doc/html/rfc7464 """ - + schema_ = expected_media.itemSchema import jsonseq.decode async def aiter_json(response: httpx2.Response) -> AsyncIterator["JSON"]: @@ -531,6 +538,7 @@ async def aiter_json(response: httpx2.Response) -> AsyncIterator["JSON"]: https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream https://github.com/mpetazzoni/sseclient/blob/main/sseclient/__init__.py#L36 """ + schema_ = expected_media.itemSchema async def aiter_json(response: httpx2.Response) -> AsyncIterator["JSON"]: @@ -552,7 +560,12 @@ async def aiter_json(response: httpx2.Response) -> AsyncIterator["JSON"]: v[cmd or "comment"] = value.lstrip() data_ = "" yield v - elif False: + elif content_type == "application/json": + """ + this will iter arrays for OpenAPI 3.x + """ + + schema_ = expected_media.schema_.items import ijson class ReadEventStream: diff --git a/src/aiopenapi3/v30/glue.py b/src/aiopenapi3/v30/glue.py index 123ed877..0878bd1d 100644 --- a/src/aiopenapi3/v30/glue.py +++ b/src/aiopenapi3/v30/glue.py @@ -49,11 +49,13 @@ ) from ..v31.paths import MediaType as v31MediaType from ..v31.paths import Response as v31Response + from ..v32.paths import MediaType as v32MediaType + from ..v32.paths import Response as v32Response from .paths import MediaType as v30MediaType from .paths import Response as v30Response - v3xResponseType = v30Response | v31Response - v3xMediaTypeType = v30MediaType | v31MediaType + v3xResponseType = v30Response | v31Response | v32Response + v3xMediaTypeType = v30MediaType | v31MediaType | v32MediaType class Request(RequestBase): @@ -581,7 +583,9 @@ def _process_stream(self, result: httpx2.Response) -> tuple["ResponseHeadersType return headers, expected_media.schema_ - def _process_sequence(self, result: httpx2.Response) -> tuple["ResponseHeadersType", Optional["SchemaType"], str]: + def _process_sequence( + self, result: httpx2.Response + ) -> tuple["ResponseHeadersType", Optional["v3xMediaTypeType"], str]: status_code = str(result.status_code) content_type = result.headers.get("Content-Type", None) @@ -590,7 +594,7 @@ def _process_sequence(self, result: httpx2.Response) -> tuple["ResponseHeadersTy headers = self._process__headers(result, result.headers, expected_response) - return headers, expected_media.itemSchema, content_type + return headers, expected_media, content_type def _process_request(self, result: httpx2.Response) -> tuple["ResponseHeadersType", "ResponseDataType"]: rheaders = {} diff --git a/tests/sequential_test.py b/tests/sequential_test.py index 6f7cb38f..78daccb5 100644 --- a/tests/sequential_test.py +++ b/tests/sequential_test.py @@ -88,6 +88,11 @@ async def sse() -> AsyncIterable[ServerSentEvent]: yield ServerSentEvent(comment=str(idx), data=item) +@app.get("/array", operation_id="array") +async def array() -> list[Item]: + return items + + @pytest.mark.asyncio(loop_scope="session") async def test_jsonl(server, client): req = client.createRequest("jsonl") @@ -105,3 +110,14 @@ async def test_sse(server, client): async with req.sequence() as sequence: async for obj in sequence: print(obj) + + +@pytest.mark.asyncio(loop_scope="session") +async def test_array(server, client): + from aiopenapi3.request import AsyncRequestBase + + req: AsyncRequestBase + req = client.createRequest("array") + async with req.sequence() as sequence: + async for obj in sequence: + print(obj)