meta-sam-parser 0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- meta_sam_parser/__init__.py +99 -0
- meta_sam_parser/_errors.py +116 -0
- meta_sam_parser/_mask_codec.py +539 -0
- meta_sam_parser/_mask_conversion.py +367 -0
- meta_sam_parser/_segmentation.py +533 -0
- meta_sam_parser/_stream.py +618 -0
- meta_sam_parser/_types.py +224 -0
- meta_sam_parser/py.typed +0 -0
- meta_sam_parser-0.0.2.dist-info/METADATA +394 -0
- meta_sam_parser-0.0.2.dist-info/RECORD +12 -0
- meta_sam_parser-0.0.2.dist-info/WHEEL +4 -0
- meta_sam_parser-0.0.2.dist-info/licenses/LICENSE +61 -0
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved.
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import math
|
|
7
|
+
from collections import deque
|
|
8
|
+
from collections.abc import AsyncIterable, AsyncIterator, Awaitable, Mapping
|
|
9
|
+
from contextlib import suppress
|
|
10
|
+
from inspect import isawaitable
|
|
11
|
+
from types import TracebackType
|
|
12
|
+
from typing import Generic, Literal, TypeVar, cast
|
|
13
|
+
|
|
14
|
+
from ._errors import (
|
|
15
|
+
ResponsesStreamAbortedError,
|
|
16
|
+
ResponsesStreamConsumedError,
|
|
17
|
+
ResponsesStreamError,
|
|
18
|
+
ResponsesStreamEventError,
|
|
19
|
+
ResponsesStreamFailedError,
|
|
20
|
+
ResponsesStreamLaneError,
|
|
21
|
+
ResponsesStreamParserError,
|
|
22
|
+
ResponsesStreamRefusalError,
|
|
23
|
+
ResponsesStreamSourceError,
|
|
24
|
+
)
|
|
25
|
+
from ._types import (
|
|
26
|
+
CompletedOutcome,
|
|
27
|
+
IncompleteOutcome,
|
|
28
|
+
OutputTextLane,
|
|
29
|
+
ParserFinish,
|
|
30
|
+
ResponseFormat,
|
|
31
|
+
ResponseFormatParser,
|
|
32
|
+
ResponsesEventLike,
|
|
33
|
+
ResponseStreamOutcome,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
EventT = TypeVar("EventT")
|
|
37
|
+
ResultT = TypeVar("ResultT")
|
|
38
|
+
ObservedT = TypeVar("ObservedT")
|
|
39
|
+
_Consumption = Literal["idle", "iteration", "final"]
|
|
40
|
+
_MAXIMUM_SAFE_INTEGER = (1 << 53) - 1
|
|
41
|
+
_MISSING = object()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _consume_future_exception(future: asyncio.Future[ObservedT]) -> None:
|
|
45
|
+
if future.cancelled():
|
|
46
|
+
return
|
|
47
|
+
with suppress(BaseException):
|
|
48
|
+
future.exception()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _field(value: object, key: str) -> object:
|
|
52
|
+
if isinstance(value, Mapping):
|
|
53
|
+
return value.get(key, _MISSING)
|
|
54
|
+
try:
|
|
55
|
+
return getattr(value, key)
|
|
56
|
+
except AttributeError:
|
|
57
|
+
return _MISSING
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _nested_string(value: object, *path: str) -> str | None:
|
|
61
|
+
current = value
|
|
62
|
+
for key in path:
|
|
63
|
+
current = _field(current, key)
|
|
64
|
+
if current is _MISSING:
|
|
65
|
+
return None
|
|
66
|
+
return current if isinstance(current, str) else None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _safe_nonnegative_integer(value: object) -> int | None:
|
|
70
|
+
if isinstance(value, bool):
|
|
71
|
+
return None
|
|
72
|
+
if isinstance(value, int):
|
|
73
|
+
integer = value
|
|
74
|
+
elif isinstance(value, float) and math.isfinite(value) and value.is_integer():
|
|
75
|
+
integer = int(value)
|
|
76
|
+
else:
|
|
77
|
+
return None
|
|
78
|
+
return integer if 0 <= integer <= _MAXIMUM_SAFE_INTEGER else None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ParsedResponsesStream(AsyncIterator[EventT], Generic[EventT, ResultT]):
|
|
82
|
+
"""One-owner async adapter from Responses API events to parsed snapshots."""
|
|
83
|
+
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
source: AsyncIterable[ResponsesEventLike],
|
|
87
|
+
format_: ResponseFormat[EventT, ResultT],
|
|
88
|
+
) -> None:
|
|
89
|
+
self._source = source
|
|
90
|
+
self._format = format_
|
|
91
|
+
self._loop: asyncio.AbstractEventLoop | None = None
|
|
92
|
+
self._parser: ResponseFormatParser[EventT, ResultT] | None = None
|
|
93
|
+
self._source_iterator: AsyncIterator[ResponsesEventLike] | None = None
|
|
94
|
+
self._consumption: _Consumption = "idle"
|
|
95
|
+
self._pull_lock = asyncio.Lock()
|
|
96
|
+
self._pending: deque[EventT] = deque()
|
|
97
|
+
self._drain_task: asyncio.Task[None] | None = None
|
|
98
|
+
self._close_task: asyncio.Task[None] | None = None
|
|
99
|
+
self._failure_task: asyncio.Task[None] | None = None
|
|
100
|
+
self._result_future: asyncio.Future[ResultT] | None = None
|
|
101
|
+
self._read_interruption: asyncio.Future[BaseException] | None = None
|
|
102
|
+
self._active_read: asyncio.Future[ResponsesEventLike] | None = None
|
|
103
|
+
self._terminal = False
|
|
104
|
+
self._consumer_done = False
|
|
105
|
+
self._result_value: ResultT | None = None
|
|
106
|
+
self._has_result = False
|
|
107
|
+
self._error: BaseException | None = None
|
|
108
|
+
self._lane: OutputTextLane | None = None
|
|
109
|
+
self._lane_done = False
|
|
110
|
+
self._text = ""
|
|
111
|
+
self._saw_delta = False
|
|
112
|
+
|
|
113
|
+
def __aiter__(self) -> ParsedResponsesStream[EventT, ResultT]:
|
|
114
|
+
if self._consumption != "idle":
|
|
115
|
+
raise ResponsesStreamConsumedError()
|
|
116
|
+
self._consumption = "iteration"
|
|
117
|
+
return self
|
|
118
|
+
|
|
119
|
+
async def __anext__(self) -> EventT:
|
|
120
|
+
if self._consumption != "iteration":
|
|
121
|
+
raise ResponsesStreamConsumedError()
|
|
122
|
+
try:
|
|
123
|
+
async with self._pull_lock:
|
|
124
|
+
return await self._next_one()
|
|
125
|
+
except asyncio.CancelledError:
|
|
126
|
+
await self._abort_after_cancellation()
|
|
127
|
+
raise
|
|
128
|
+
|
|
129
|
+
async def __aenter__(self) -> ParsedResponsesStream[EventT, ResultT]:
|
|
130
|
+
self._ensure_loop()
|
|
131
|
+
return self
|
|
132
|
+
|
|
133
|
+
async def __aexit__(
|
|
134
|
+
self,
|
|
135
|
+
exc_type: type[BaseException] | None,
|
|
136
|
+
exc_value: BaseException | None,
|
|
137
|
+
traceback: TracebackType | None,
|
|
138
|
+
) -> Literal[False]:
|
|
139
|
+
await self.aclose()
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
def final_result(self) -> Awaitable[ResultT]:
|
|
143
|
+
"""Claim final-only mode immediately and await the shared terminal result."""
|
|
144
|
+
|
|
145
|
+
self._ensure_loop()
|
|
146
|
+
result = self._ensure_result_future()
|
|
147
|
+
if self._consumption == "idle":
|
|
148
|
+
self._consumption = "final"
|
|
149
|
+
self._drain_task = self._ensure_loop().create_task(self._drain_final())
|
|
150
|
+
self._observe(self._drain_task)
|
|
151
|
+
return self._await_result(result)
|
|
152
|
+
|
|
153
|
+
async def _await_result(self, result: asyncio.Future[ResultT]) -> ResultT:
|
|
154
|
+
try:
|
|
155
|
+
return await asyncio.shield(result)
|
|
156
|
+
except asyncio.CancelledError:
|
|
157
|
+
await self._abort_after_cancellation()
|
|
158
|
+
raise
|
|
159
|
+
|
|
160
|
+
async def aclose(self) -> None:
|
|
161
|
+
"""Best-effort close; abandoning a live stream rejects its final result."""
|
|
162
|
+
|
|
163
|
+
self._ensure_loop()
|
|
164
|
+
current = asyncio.current_task()
|
|
165
|
+
if current is self._close_task or current is self._failure_task:
|
|
166
|
+
return
|
|
167
|
+
if self._consumer_done:
|
|
168
|
+
return
|
|
169
|
+
if self._consumption == "idle":
|
|
170
|
+
self._consumption = "final"
|
|
171
|
+
failure = self._start_failure(ResponsesStreamAbortedError())
|
|
172
|
+
try:
|
|
173
|
+
await asyncio.shield(failure)
|
|
174
|
+
except asyncio.CancelledError:
|
|
175
|
+
raise
|
|
176
|
+
self._consumer_done = True
|
|
177
|
+
if (
|
|
178
|
+
isinstance(self._error, ResponsesStreamSourceError)
|
|
179
|
+
and self._error.operation == "return"
|
|
180
|
+
):
|
|
181
|
+
raise self._error
|
|
182
|
+
|
|
183
|
+
async def _next_one(self) -> EventT:
|
|
184
|
+
if self._pending:
|
|
185
|
+
return self._pending.popleft()
|
|
186
|
+
if self._error is not None:
|
|
187
|
+
raise self._error
|
|
188
|
+
if self._terminal:
|
|
189
|
+
self._complete_iteration()
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
self._ensure_source_iterator()
|
|
193
|
+
self._ensure_parser()
|
|
194
|
+
while True:
|
|
195
|
+
done, event = await self._read_source()
|
|
196
|
+
if self._terminal:
|
|
197
|
+
if self._error is not None:
|
|
198
|
+
raise self._error
|
|
199
|
+
self._complete_iteration()
|
|
200
|
+
if done:
|
|
201
|
+
self._finish(IncompleteOutcome(reason="eof"), emit=True)
|
|
202
|
+
await self._close_source()
|
|
203
|
+
if self._pending:
|
|
204
|
+
return self._pending.popleft()
|
|
205
|
+
self._complete_iteration()
|
|
206
|
+
assert event is not None
|
|
207
|
+
outcome = self._accept(event, emit=True)
|
|
208
|
+
if outcome is not None:
|
|
209
|
+
self._finish(outcome, emit=True)
|
|
210
|
+
await self._close_source()
|
|
211
|
+
if self._pending:
|
|
212
|
+
return self._pending.popleft()
|
|
213
|
+
self._complete_iteration()
|
|
214
|
+
if self._pending:
|
|
215
|
+
return self._pending.popleft()
|
|
216
|
+
except StopAsyncIteration:
|
|
217
|
+
raise
|
|
218
|
+
except asyncio.CancelledError:
|
|
219
|
+
raise
|
|
220
|
+
except Exception as error:
|
|
221
|
+
await asyncio.shield(self._start_failure(error))
|
|
222
|
+
assert self._error is not None
|
|
223
|
+
raise self._error from self._error.__cause__
|
|
224
|
+
raise AssertionError("unreachable")
|
|
225
|
+
|
|
226
|
+
async def _drain_final(self) -> None:
|
|
227
|
+
try:
|
|
228
|
+
self._ensure_source_iterator()
|
|
229
|
+
self._ensure_parser()
|
|
230
|
+
while True:
|
|
231
|
+
done, event = await self._read_source()
|
|
232
|
+
if done:
|
|
233
|
+
self._finish(IncompleteOutcome(reason="eof"), emit=False)
|
|
234
|
+
await self._close_source()
|
|
235
|
+
self._consumer_done = True
|
|
236
|
+
self._resolve_result()
|
|
237
|
+
return
|
|
238
|
+
assert event is not None
|
|
239
|
+
outcome = self._accept(event, emit=False)
|
|
240
|
+
if outcome is not None:
|
|
241
|
+
self._finish(outcome, emit=False)
|
|
242
|
+
await self._close_source()
|
|
243
|
+
self._consumer_done = True
|
|
244
|
+
self._resolve_result()
|
|
245
|
+
return
|
|
246
|
+
except asyncio.CancelledError:
|
|
247
|
+
await self._abort_after_cancellation()
|
|
248
|
+
raise
|
|
249
|
+
except Exception as error:
|
|
250
|
+
await asyncio.shield(self._start_failure(error))
|
|
251
|
+
|
|
252
|
+
def _accept(
|
|
253
|
+
self, event: ResponsesEventLike, *, emit: bool
|
|
254
|
+
) -> ResponseStreamOutcome | None:
|
|
255
|
+
event_type = _field(event, "type")
|
|
256
|
+
assert isinstance(event_type, str)
|
|
257
|
+
if event_type == "response.output_text.delta":
|
|
258
|
+
lane = self._accept_lane(event)
|
|
259
|
+
if self._lane_done:
|
|
260
|
+
raise ResponsesStreamLaneError(
|
|
261
|
+
"The response emitted output text after finalizing its lane.",
|
|
262
|
+
self._lane,
|
|
263
|
+
lane,
|
|
264
|
+
)
|
|
265
|
+
delta = _field(event, "delta")
|
|
266
|
+
if not isinstance(delta, str):
|
|
267
|
+
raise ResponsesStreamLaneError(
|
|
268
|
+
"The output text delta is missing its text.",
|
|
269
|
+
self._lane,
|
|
270
|
+
lane,
|
|
271
|
+
)
|
|
272
|
+
self._saw_delta = True
|
|
273
|
+
self._text += delta
|
|
274
|
+
self._push_parser(delta, emit=emit)
|
|
275
|
+
return None
|
|
276
|
+
if event_type == "response.output_text.done":
|
|
277
|
+
lane = self._accept_lane(event)
|
|
278
|
+
if self._lane_done:
|
|
279
|
+
raise ResponsesStreamLaneError(
|
|
280
|
+
"The response finalized its output text lane more than once.",
|
|
281
|
+
self._lane,
|
|
282
|
+
lane,
|
|
283
|
+
)
|
|
284
|
+
text = _field(event, "text")
|
|
285
|
+
if not isinstance(text, str):
|
|
286
|
+
raise ResponsesStreamLaneError(
|
|
287
|
+
"The completed output text is missing its text.",
|
|
288
|
+
self._lane,
|
|
289
|
+
lane,
|
|
290
|
+
)
|
|
291
|
+
if self._saw_delta and text != self._text:
|
|
292
|
+
raise ResponsesStreamLaneError(
|
|
293
|
+
"The finalized output text conflicts with its accumulated deltas.",
|
|
294
|
+
self._lane,
|
|
295
|
+
lane,
|
|
296
|
+
)
|
|
297
|
+
if not self._saw_delta:
|
|
298
|
+
self._text = text
|
|
299
|
+
self._push_parser(text, emit=emit)
|
|
300
|
+
self._lane_done = True
|
|
301
|
+
return None
|
|
302
|
+
if event_type in ("response.refusal.delta", "response.refusal.done"):
|
|
303
|
+
refusal = _field(event, "refusal")
|
|
304
|
+
delta = _field(event, "delta")
|
|
305
|
+
message = (
|
|
306
|
+
refusal
|
|
307
|
+
if isinstance(refusal, str)
|
|
308
|
+
else delta
|
|
309
|
+
if isinstance(delta, str)
|
|
310
|
+
else "The response was refused."
|
|
311
|
+
)
|
|
312
|
+
raise ResponsesStreamRefusalError(message, event)
|
|
313
|
+
if event_type == "response.completed":
|
|
314
|
+
if self._lane is None or not self._lane_done:
|
|
315
|
+
raise ResponsesStreamLaneError(
|
|
316
|
+
"The response completed before finalizing one output text lane.",
|
|
317
|
+
self._lane,
|
|
318
|
+
None,
|
|
319
|
+
)
|
|
320
|
+
return CompletedOutcome()
|
|
321
|
+
if event_type == "response.incomplete":
|
|
322
|
+
if self._lane is None or not self._lane_done:
|
|
323
|
+
raise ResponsesStreamLaneError(
|
|
324
|
+
"The response became incomplete before finalizing one "
|
|
325
|
+
"output text lane.",
|
|
326
|
+
self._lane,
|
|
327
|
+
None,
|
|
328
|
+
)
|
|
329
|
+
detail = _nested_string(event, "response", "incomplete_details", "reason")
|
|
330
|
+
return IncompleteOutcome(reason="response", detail=detail)
|
|
331
|
+
if event_type == "response.failed":
|
|
332
|
+
failed_message = _nested_string(event, "response", "error", "message")
|
|
333
|
+
if failed_message is None:
|
|
334
|
+
failed_message = "The response failed."
|
|
335
|
+
raise ResponsesStreamFailedError(failed_message, event)
|
|
336
|
+
if event_type == "error":
|
|
337
|
+
event_message = _nested_string(event, "error", "message")
|
|
338
|
+
if event_message is None:
|
|
339
|
+
event_message = _nested_string(event, "message")
|
|
340
|
+
if event_message is None:
|
|
341
|
+
event_message = "The Responses stream reported an error."
|
|
342
|
+
raise ResponsesStreamEventError(event_message, event)
|
|
343
|
+
return None
|
|
344
|
+
|
|
345
|
+
def _accept_lane(self, event: ResponsesEventLike) -> OutputTextLane:
|
|
346
|
+
item_id = _field(event, "item_id")
|
|
347
|
+
output_index = _safe_nonnegative_integer(_field(event, "output_index"))
|
|
348
|
+
content_index = _safe_nonnegative_integer(_field(event, "content_index"))
|
|
349
|
+
lane = (
|
|
350
|
+
OutputTextLane(item_id, output_index, content_index)
|
|
351
|
+
if isinstance(item_id, str)
|
|
352
|
+
and item_id
|
|
353
|
+
and output_index is not None
|
|
354
|
+
and content_index is not None
|
|
355
|
+
else None
|
|
356
|
+
)
|
|
357
|
+
if lane is None:
|
|
358
|
+
raise ResponsesStreamLaneError(
|
|
359
|
+
"The output text event has an invalid lane identity.",
|
|
360
|
+
self._lane,
|
|
361
|
+
None,
|
|
362
|
+
)
|
|
363
|
+
if self._lane is None:
|
|
364
|
+
self._lane = lane
|
|
365
|
+
return lane
|
|
366
|
+
if self._lane != lane:
|
|
367
|
+
raise ResponsesStreamLaneError(
|
|
368
|
+
"The response interleaved multiple output text lanes.",
|
|
369
|
+
self._lane,
|
|
370
|
+
lane,
|
|
371
|
+
)
|
|
372
|
+
return lane
|
|
373
|
+
|
|
374
|
+
def _push_parser(self, chunk: str, *, emit: bool) -> None:
|
|
375
|
+
try:
|
|
376
|
+
events = self._ensure_parser().push(chunk, emit=emit)
|
|
377
|
+
if emit:
|
|
378
|
+
self._pending.extend(events)
|
|
379
|
+
except asyncio.CancelledError:
|
|
380
|
+
raise
|
|
381
|
+
except ResponsesStreamError:
|
|
382
|
+
raise
|
|
383
|
+
except Exception as error:
|
|
384
|
+
raise ResponsesStreamParserError(error) from error
|
|
385
|
+
|
|
386
|
+
def _finish(self, outcome: ResponseStreamOutcome, *, emit: bool) -> None:
|
|
387
|
+
if self._terminal:
|
|
388
|
+
return
|
|
389
|
+
try:
|
|
390
|
+
finished = self._ensure_parser().finish(outcome)
|
|
391
|
+
if not isinstance(finished, ParserFinish):
|
|
392
|
+
raise TypeError("Parser finish() must return ParserFinish.")
|
|
393
|
+
if emit:
|
|
394
|
+
self._pending.extend(finished.events)
|
|
395
|
+
self._result_value = finished.result
|
|
396
|
+
except asyncio.CancelledError:
|
|
397
|
+
raise
|
|
398
|
+
except ResponsesStreamError:
|
|
399
|
+
raise
|
|
400
|
+
except Exception as error:
|
|
401
|
+
raise ResponsesStreamParserError(error) from error
|
|
402
|
+
self._has_result = True
|
|
403
|
+
self._terminal = True
|
|
404
|
+
|
|
405
|
+
def _ensure_parser(self) -> ResponseFormatParser[EventT, ResultT]:
|
|
406
|
+
if self._parser is not None:
|
|
407
|
+
return self._parser
|
|
408
|
+
try:
|
|
409
|
+
self._parser = self._format.create_parser()
|
|
410
|
+
except asyncio.CancelledError:
|
|
411
|
+
raise
|
|
412
|
+
except ResponsesStreamError:
|
|
413
|
+
raise
|
|
414
|
+
except Exception as error:
|
|
415
|
+
raise ResponsesStreamParserError(error) from error
|
|
416
|
+
return self._parser
|
|
417
|
+
|
|
418
|
+
def _complete_iteration(self) -> None:
|
|
419
|
+
if self._error is not None:
|
|
420
|
+
raise self._error
|
|
421
|
+
self._consumer_done = True
|
|
422
|
+
self._resolve_result()
|
|
423
|
+
raise StopAsyncIteration
|
|
424
|
+
|
|
425
|
+
def _resolve_result(self) -> None:
|
|
426
|
+
if not self._has_result or self._error is not None:
|
|
427
|
+
return
|
|
428
|
+
future = self._result_future
|
|
429
|
+
if future is not None and not future.done():
|
|
430
|
+
future.set_result(cast(ResultT, self._result_value))
|
|
431
|
+
|
|
432
|
+
def _start_failure(self, error: BaseException) -> asyncio.Task[None]:
|
|
433
|
+
if self._failure_task is not None:
|
|
434
|
+
return self._failure_task
|
|
435
|
+
failure = (
|
|
436
|
+
error
|
|
437
|
+
if isinstance(error, ResponsesStreamError)
|
|
438
|
+
else ResponsesStreamParserError(error)
|
|
439
|
+
)
|
|
440
|
+
self._failure_task = self._ensure_loop().create_task(
|
|
441
|
+
self._fail_and_close(failure)
|
|
442
|
+
)
|
|
443
|
+
self._observe(self._failure_task)
|
|
444
|
+
return self._failure_task
|
|
445
|
+
|
|
446
|
+
async def _fail_and_close(self, failure: ResponsesStreamError) -> None:
|
|
447
|
+
self._interrupt_reads(failure)
|
|
448
|
+
active = self._active_read
|
|
449
|
+
if active is not None and not active.done():
|
|
450
|
+
active.cancel()
|
|
451
|
+
with suppress(BaseException):
|
|
452
|
+
await active
|
|
453
|
+
if (
|
|
454
|
+
isinstance(failure, ResponsesStreamSourceError)
|
|
455
|
+
and failure.operation == "return"
|
|
456
|
+
and self._close_task is not None
|
|
457
|
+
):
|
|
458
|
+
self._fail(failure)
|
|
459
|
+
return
|
|
460
|
+
try:
|
|
461
|
+
await self._close_source()
|
|
462
|
+
except ResponsesStreamSourceError as close_error:
|
|
463
|
+
cause = close_error.cause
|
|
464
|
+
assert cause is not None
|
|
465
|
+
failure = ResponsesStreamSourceError("return", cause, prior_error=failure)
|
|
466
|
+
self._fail(failure)
|
|
467
|
+
|
|
468
|
+
def _interrupt_reads(self, error: BaseException) -> None:
|
|
469
|
+
future = self._ensure_read_interruption()
|
|
470
|
+
if not future.done():
|
|
471
|
+
future.set_result(error)
|
|
472
|
+
|
|
473
|
+
def _fail(self, error: BaseException) -> None:
|
|
474
|
+
if self._error is not None:
|
|
475
|
+
return
|
|
476
|
+
self._error = error
|
|
477
|
+
self._terminal = True
|
|
478
|
+
self._pending.clear()
|
|
479
|
+
future = self._result_future
|
|
480
|
+
if future is not None and not future.done():
|
|
481
|
+
future.set_exception(error)
|
|
482
|
+
|
|
483
|
+
async def _abort_after_cancellation(self) -> None:
|
|
484
|
+
if self._consumer_done:
|
|
485
|
+
return
|
|
486
|
+
failure = self._start_failure(ResponsesStreamAbortedError())
|
|
487
|
+
with suppress(asyncio.CancelledError):
|
|
488
|
+
await asyncio.shield(failure)
|
|
489
|
+
self._consumer_done = True
|
|
490
|
+
|
|
491
|
+
def _ensure_source_iterator(self) -> AsyncIterator[ResponsesEventLike]:
|
|
492
|
+
if self._source_iterator is not None:
|
|
493
|
+
return self._source_iterator
|
|
494
|
+
try:
|
|
495
|
+
self._source_iterator = aiter(self._source)
|
|
496
|
+
except asyncio.CancelledError:
|
|
497
|
+
raise
|
|
498
|
+
except Exception as error:
|
|
499
|
+
raise ResponsesStreamSourceError("iterator", error) from error
|
|
500
|
+
return self._source_iterator
|
|
501
|
+
|
|
502
|
+
async def _read_source(self) -> tuple[bool, ResponsesEventLike | None]:
|
|
503
|
+
iterator = self._ensure_source_iterator()
|
|
504
|
+
try:
|
|
505
|
+
awaitable = iterator.__anext__()
|
|
506
|
+
reading = asyncio.ensure_future(awaitable)
|
|
507
|
+
except StopAsyncIteration:
|
|
508
|
+
return True, None
|
|
509
|
+
except asyncio.CancelledError:
|
|
510
|
+
raise
|
|
511
|
+
except Exception as error:
|
|
512
|
+
raise ResponsesStreamSourceError("next", error) from error
|
|
513
|
+
self._active_read = reading
|
|
514
|
+
self._observe(reading)
|
|
515
|
+
interrupted = self._ensure_read_interruption()
|
|
516
|
+
try:
|
|
517
|
+
completed, _pending = await asyncio.wait(
|
|
518
|
+
(reading, interrupted), return_when=asyncio.FIRST_COMPLETED
|
|
519
|
+
)
|
|
520
|
+
if interrupted in completed:
|
|
521
|
+
interrupted_error = interrupted.result()
|
|
522
|
+
reading.cancel()
|
|
523
|
+
raise interrupted_error
|
|
524
|
+
try:
|
|
525
|
+
event = reading.result()
|
|
526
|
+
except StopAsyncIteration:
|
|
527
|
+
return True, None
|
|
528
|
+
except asyncio.CancelledError:
|
|
529
|
+
raise
|
|
530
|
+
except Exception as error:
|
|
531
|
+
raise ResponsesStreamSourceError("next", error) from error
|
|
532
|
+
try:
|
|
533
|
+
event_type = _field(event, "type")
|
|
534
|
+
except Exception as error:
|
|
535
|
+
raise ResponsesStreamSourceError("next", error) from error
|
|
536
|
+
if not isinstance(event_type, str):
|
|
537
|
+
shape_error = TypeError(
|
|
538
|
+
"The event source yielded an invalid Responses event."
|
|
539
|
+
)
|
|
540
|
+
raise ResponsesStreamSourceError("next", shape_error) from shape_error
|
|
541
|
+
return False, event
|
|
542
|
+
except asyncio.CancelledError:
|
|
543
|
+
reading.cancel()
|
|
544
|
+
with suppress(BaseException):
|
|
545
|
+
await reading
|
|
546
|
+
raise
|
|
547
|
+
finally:
|
|
548
|
+
if self._active_read is reading:
|
|
549
|
+
self._active_read = None
|
|
550
|
+
|
|
551
|
+
async def _close_source(self) -> None:
|
|
552
|
+
current = asyncio.current_task()
|
|
553
|
+
if current is self._close_task:
|
|
554
|
+
return
|
|
555
|
+
if self._close_task is None:
|
|
556
|
+
self._close_task = self._ensure_loop().create_task(self._run_close_source())
|
|
557
|
+
self._observe(self._close_task)
|
|
558
|
+
await asyncio.shield(self._close_task)
|
|
559
|
+
|
|
560
|
+
async def _run_close_source(self) -> None:
|
|
561
|
+
iterator = self._source_iterator
|
|
562
|
+
target: object = self._source
|
|
563
|
+
try:
|
|
564
|
+
close = getattr(target, "aclose", None)
|
|
565
|
+
if close is None:
|
|
566
|
+
close = getattr(target, "close", None)
|
|
567
|
+
if close is None and iterator is not None and iterator is not target:
|
|
568
|
+
target = iterator
|
|
569
|
+
close = getattr(target, "aclose", None)
|
|
570
|
+
if close is None:
|
|
571
|
+
close = getattr(target, "close", None)
|
|
572
|
+
except BaseException as error:
|
|
573
|
+
raise ResponsesStreamSourceError("return", error) from error
|
|
574
|
+
if close is None:
|
|
575
|
+
return
|
|
576
|
+
try:
|
|
577
|
+
outcome = close()
|
|
578
|
+
if isawaitable(outcome):
|
|
579
|
+
await outcome
|
|
580
|
+
except BaseException as error:
|
|
581
|
+
raise ResponsesStreamSourceError("return", error) from error
|
|
582
|
+
|
|
583
|
+
def _ensure_loop(self) -> asyncio.AbstractEventLoop:
|
|
584
|
+
loop = asyncio.get_running_loop()
|
|
585
|
+
if self._loop is None:
|
|
586
|
+
self._loop = loop
|
|
587
|
+
elif self._loop is not loop:
|
|
588
|
+
raise RuntimeError("A parsed response stream cannot cross event loops.")
|
|
589
|
+
return loop
|
|
590
|
+
|
|
591
|
+
def _ensure_result_future(self) -> asyncio.Future[ResultT]:
|
|
592
|
+
if self._result_future is None:
|
|
593
|
+
future = self._ensure_loop().create_future()
|
|
594
|
+
self._result_future = future
|
|
595
|
+
self._observe(future)
|
|
596
|
+
if self._error is not None:
|
|
597
|
+
future.set_exception(self._error)
|
|
598
|
+
elif self._has_result and self._consumer_done:
|
|
599
|
+
future.set_result(cast(ResultT, self._result_value))
|
|
600
|
+
return self._result_future
|
|
601
|
+
|
|
602
|
+
def _ensure_read_interruption(self) -> asyncio.Future[BaseException]:
|
|
603
|
+
if self._read_interruption is None:
|
|
604
|
+
self._read_interruption = self._ensure_loop().create_future()
|
|
605
|
+
return self._read_interruption
|
|
606
|
+
|
|
607
|
+
@staticmethod
|
|
608
|
+
def _observe(future: asyncio.Future[ObservedT]) -> None:
|
|
609
|
+
future.add_done_callback(_consume_future_exception)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def parse_responses_stream(
|
|
613
|
+
source: AsyncIterable[ResponsesEventLike],
|
|
614
|
+
format: ResponseFormat[EventT, ResultT],
|
|
615
|
+
) -> ParsedResponsesStream[EventT, ResultT]:
|
|
616
|
+
"""Create a lazy, single-consumer parser for a Responses event stream."""
|
|
617
|
+
|
|
618
|
+
return ParsedResponsesStream(source, format)
|