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
7 changes: 5 additions & 2 deletions docs/source/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
25 changes: 19 additions & 6 deletions src/aiopenapi3/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -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"]:
Expand All @@ -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():
Expand All @@ -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:
Expand Down Expand Up @@ -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():
Expand All @@ -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"]:
Expand All @@ -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"]:

Expand All @@ -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:
Expand Down
12 changes: 8 additions & 4 deletions src/aiopenapi3/v30/glue.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)

Expand All @@ -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 = {}
Expand Down
16 changes: 16 additions & 0 deletions tests/sequential_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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)