ommlds 0.0.0.dev502__py3-none-any.whl → 0.0.0.dev504__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.
- ommlds/.omlish-manifests.json +14 -7
- ommlds/backends/anthropic/protocol/_dataclasses.py +16 -16
- ommlds/backends/cerebras/_dataclasses.py +42 -42
- ommlds/backends/google/protocol/_dataclasses.py +64 -64
- ommlds/backends/groq/_dataclasses.py +36 -36
- ommlds/backends/ollama/_dataclasses.py +28 -28
- ommlds/backends/openai/protocol/_dataclasses.py +88 -88
- ommlds/backends/tavily/_dataclasses.py +16 -16
- ommlds/cli/_dataclasses.py +64 -114
- ommlds/cli/backends/inject.py +20 -0
- ommlds/cli/backends/meta.py +47 -0
- ommlds/cli/sessions/chat/drivers/ai/tools.py +3 -7
- ommlds/cli/sessions/chat/facades/commands/base.py +1 -1
- ommlds/minichain/__init__.py +38 -4
- ommlds/minichain/_dataclasses.py +452 -289
- ommlds/minichain/backends/impls/anthropic/stream.py +1 -1
- ommlds/minichain/backends/impls/cerebras/names.py +15 -0
- ommlds/minichain/backends/impls/cerebras/stream.py +39 -52
- ommlds/minichain/backends/impls/google/chat.py +11 -82
- ommlds/minichain/backends/impls/google/protocol.py +105 -0
- ommlds/minichain/backends/impls/google/stream.py +49 -132
- ommlds/minichain/backends/impls/groq/stream.py +40 -53
- ommlds/minichain/backends/impls/ollama/chat.py +1 -1
- ommlds/minichain/backends/impls/openai/format.py +1 -0
- ommlds/minichain/backends/impls/openai/stream.py +40 -55
- ommlds/minichain/http/__init__.py +0 -0
- ommlds/minichain/http/stream.py +195 -0
- ommlds/minichain/resources.py +22 -1
- ommlds/minichain/stream/services.py +24 -1
- ommlds/minichain/wrappers/firstinwins.py +1 -1
- ommlds/minichain/wrappers/instrument.py +1 -1
- ommlds/minichain/wrappers/retry.py +34 -13
- {ommlds-0.0.0.dev502.dist-info → ommlds-0.0.0.dev504.dist-info}/METADATA +4 -4
- {ommlds-0.0.0.dev502.dist-info → ommlds-0.0.0.dev504.dist-info}/RECORD +38 -36
- ommlds/minichain/stream/wrap.py +0 -62
- {ommlds-0.0.0.dev502.dist-info → ommlds-0.0.0.dev504.dist-info}/WHEEL +0 -0
- {ommlds-0.0.0.dev502.dist-info → ommlds-0.0.0.dev504.dist-info}/entry_points.txt +0 -0
- {ommlds-0.0.0.dev502.dist-info → ommlds-0.0.0.dev504.dist-info}/licenses/LICENSE +0 -0
- {ommlds-0.0.0.dev502.dist-info → ommlds-0.0.0.dev504.dist-info}/top_level.txt +0 -0
|
@@ -6,21 +6,18 @@ from omlish import typedvalues as tv
|
|
|
6
6
|
from omlish.formats import json
|
|
7
7
|
from omlish.http import all as http
|
|
8
8
|
from omlish.http import sse
|
|
9
|
-
from omlish.io.buffers import DelimitingBuffer
|
|
10
9
|
|
|
11
10
|
from .....backends.groq import protocol as pt
|
|
12
11
|
from .....backends.groq.clients import REQUIRED_HTTP_HEADERS
|
|
13
|
-
from ....chat.choices.services import ChatChoicesOutputs
|
|
14
12
|
from ....chat.choices.stream.services import ChatChoicesStreamRequest
|
|
15
13
|
from ....chat.choices.stream.services import ChatChoicesStreamResponse
|
|
16
14
|
from ....chat.choices.stream.services import static_check_is_chat_choices_stream_service
|
|
17
15
|
from ....chat.choices.stream.types import AiChoicesDeltas
|
|
18
16
|
from ....chat.tools.types import Tool
|
|
19
17
|
from ....configs import Config
|
|
20
|
-
from ....
|
|
18
|
+
from ....http.stream import BytesHttpStreamResponseBuilder
|
|
19
|
+
from ....http.stream import SimpleSseLinesHttpStreamResponseHandler
|
|
21
20
|
from ....standard import ApiKey
|
|
22
|
-
from ....stream.services import StreamResponseSink
|
|
23
|
-
from ....stream.services import new_stream_response
|
|
24
21
|
from .chat import GroqChatChoicesService
|
|
25
22
|
from .names import MODEL_NAMES
|
|
26
23
|
from .protocol import build_gq_request_messages
|
|
@@ -50,6 +47,35 @@ class GroqChatChoicesStreamService:
|
|
|
50
47
|
self._model_name = cc.pop(GroqChatChoicesService.DEFAULT_MODEL_NAME)
|
|
51
48
|
self._api_key = ApiKey.pop_secret(cc, env='GROQ_API_KEY')
|
|
52
49
|
|
|
50
|
+
URL: ta.ClassVar[str] = 'https://api.groq.com/openai/v1/chat/completions'
|
|
51
|
+
|
|
52
|
+
def _process_sse(self, so: sse.SseDecoderOutput) -> ta.Sequence[AiChoicesDeltas | None]:
|
|
53
|
+
if not (isinstance(so, sse.SseEvent) and so.type == b'message'):
|
|
54
|
+
return []
|
|
55
|
+
|
|
56
|
+
ss = so.data.decode('utf-8')
|
|
57
|
+
if ss == '[DONE]':
|
|
58
|
+
return [None]
|
|
59
|
+
|
|
60
|
+
sj = json.loads(ss) # ChatCompletionChunk
|
|
61
|
+
|
|
62
|
+
check.state(sj['object'] == 'chat.completion.chunk')
|
|
63
|
+
|
|
64
|
+
ccc = msh.unmarshal(sj, pt.ChatCompletionChunk)
|
|
65
|
+
|
|
66
|
+
# FIXME: stop reason
|
|
67
|
+
if not ccc.choices:
|
|
68
|
+
return []
|
|
69
|
+
|
|
70
|
+
if any(choice.finish_reason for choice in ccc.choices):
|
|
71
|
+
check.state(all(choice.finish_reason for choice in ccc.choices))
|
|
72
|
+
return [None]
|
|
73
|
+
|
|
74
|
+
return [AiChoicesDeltas([
|
|
75
|
+
build_mc_ai_choice_deltas(choice.delta)
|
|
76
|
+
for choice in ccc.choices
|
|
77
|
+
])]
|
|
78
|
+
|
|
53
79
|
READ_CHUNK_SIZE: ta.ClassVar[int] = -1
|
|
54
80
|
|
|
55
81
|
async def invoke(self, request: ChatChoicesStreamRequest) -> ChatChoicesStreamResponse:
|
|
@@ -69,7 +95,7 @@ class GroqChatChoicesStreamService:
|
|
|
69
95
|
raw_request = msh.marshal(gq_request)
|
|
70
96
|
|
|
71
97
|
http_request = http.HttpRequest(
|
|
72
|
-
|
|
98
|
+
self.URL,
|
|
73
99
|
headers={
|
|
74
100
|
http.consts.HEADER_CONTENT_TYPE: http.consts.CONTENT_TYPE_JSON,
|
|
75
101
|
http.consts.HEADER_AUTH: http.consts.format_bearer_auth_header(check.not_none(self._api_key).reveal()),
|
|
@@ -78,50 +104,11 @@ class GroqChatChoicesStreamService:
|
|
|
78
104
|
data=json.dumps(raw_request).encode('utf-8'),
|
|
79
105
|
)
|
|
80
106
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
http_response
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
b = await http_response.stream.read1(self.READ_CHUNK_SIZE)
|
|
90
|
-
for l in db.feed(b):
|
|
91
|
-
if isinstance(l, DelimitingBuffer.Incomplete):
|
|
92
|
-
# FIXME: handle
|
|
93
|
-
return []
|
|
94
|
-
|
|
95
|
-
# FIXME: https://platform.openai.com/docs/guides/function-calling?api-mode=responses#streaming
|
|
96
|
-
for so in sd.process_line(l):
|
|
97
|
-
if isinstance(so, sse.SseEvent) and so.type == b'message':
|
|
98
|
-
ss = so.data.decode('utf-8')
|
|
99
|
-
if ss == '[DONE]':
|
|
100
|
-
return []
|
|
101
|
-
|
|
102
|
-
sj = json.loads(ss) # ChatCompletionChunk
|
|
103
|
-
|
|
104
|
-
check.state(sj['object'] == 'chat.completion.chunk')
|
|
105
|
-
|
|
106
|
-
ccc = msh.unmarshal(sj, pt.ChatCompletionChunk)
|
|
107
|
-
|
|
108
|
-
# FIXME: stop reason
|
|
109
|
-
if not ccc.choices:
|
|
110
|
-
continue
|
|
111
|
-
|
|
112
|
-
if any(choice.finish_reason for choice in ccc.choices):
|
|
113
|
-
check.state(all(choice.finish_reason for choice in ccc.choices))
|
|
114
|
-
break
|
|
115
|
-
|
|
116
|
-
await sink.emit(AiChoicesDeltas([
|
|
117
|
-
build_mc_ai_choice_deltas(choice.delta)
|
|
118
|
-
for choice in ccc.choices
|
|
119
|
-
]))
|
|
120
|
-
|
|
121
|
-
if not b:
|
|
122
|
-
return []
|
|
123
|
-
|
|
124
|
-
# raw_response = json.loads(check.not_none(http_response.data).decode('utf-8'))
|
|
125
|
-
# return rh.build_response(raw_response)
|
|
126
|
-
|
|
127
|
-
return await new_stream_response(rs, inner)
|
|
107
|
+
return await BytesHttpStreamResponseBuilder(
|
|
108
|
+
self._http_client,
|
|
109
|
+
lambda http_response: SimpleSseLinesHttpStreamResponseHandler(self._process_sse).as_lines().as_bytes(),
|
|
110
|
+
read_chunk_size=self.READ_CHUNK_SIZE,
|
|
111
|
+
).new_stream_response(
|
|
112
|
+
http_request,
|
|
113
|
+
request.options,
|
|
114
|
+
)
|
|
@@ -179,7 +179,7 @@ class OllamaChatChoicesStreamService(BaseOllamaChatChoicesService):
|
|
|
179
179
|
for l in db.feed(b):
|
|
180
180
|
if isinstance(l, DelimitingBuffer.Incomplete):
|
|
181
181
|
# FIXME: handle
|
|
182
|
-
|
|
182
|
+
raise TypeError(l)
|
|
183
183
|
|
|
184
184
|
lj = json.loads(l.decode('utf-8'))
|
|
185
185
|
lp: pt.ChatResponse = msh.unmarshal(lj, pt.ChatResponse)
|
|
@@ -9,10 +9,8 @@ from omlish import typedvalues as tv
|
|
|
9
9
|
from omlish.formats import json
|
|
10
10
|
from omlish.http import all as http
|
|
11
11
|
from omlish.http import sse
|
|
12
|
-
from omlish.io.buffers import DelimitingBuffer
|
|
13
12
|
|
|
14
13
|
from .....backends.openai import protocol as pt
|
|
15
|
-
from ....chat.choices.services import ChatChoicesOutputs
|
|
16
14
|
from ....chat.choices.stream.services import ChatChoicesStreamRequest
|
|
17
15
|
from ....chat.choices.stream.services import ChatChoicesStreamResponse
|
|
18
16
|
from ....chat.choices.stream.services import static_check_is_chat_choices_stream_service
|
|
@@ -20,12 +18,11 @@ from ....chat.choices.stream.types import AiChoiceDeltas
|
|
|
20
18
|
from ....chat.choices.stream.types import AiChoicesDeltas
|
|
21
19
|
from ....chat.choices.stream.types import ChatChoicesStreamOption
|
|
22
20
|
from ....configs import Config
|
|
21
|
+
from ....http.stream import BytesHttpStreamResponseBuilder
|
|
22
|
+
from ....http.stream import SimpleSseLinesHttpStreamResponseHandler
|
|
23
23
|
from ....resources import ResourcesOption
|
|
24
|
-
from ....resources import UseResources
|
|
25
24
|
from ....standard import ApiKey
|
|
26
25
|
from ....stream.services import StreamOption
|
|
27
|
-
from ....stream.services import StreamResponseSink
|
|
28
|
-
from ....stream.services import new_stream_response
|
|
29
26
|
from .chat import OpenaiChatChoicesService
|
|
30
27
|
from .format import OpenaiChatRequestHandler
|
|
31
28
|
from .format import build_mc_ai_delta
|
|
@@ -54,11 +51,38 @@ class OpenaiChatChoicesStreamService:
|
|
|
54
51
|
self._model_name = cc.pop(OpenaiChatChoicesService.DEFAULT_MODEL_NAME)
|
|
55
52
|
self._api_key = ApiKey.pop_secret(cc, env='OPENAI_API_KEY')
|
|
56
53
|
|
|
54
|
+
URL: ta.ClassVar[str] = 'https://api.openai.com/v1/chat/completions'
|
|
55
|
+
|
|
56
|
+
def _process_sse(self, so: sse.SseDecoderOutput) -> ta.Sequence[AiChoicesDeltas | None]:
|
|
57
|
+
if not (isinstance(so, sse.SseEvent) and so.type == b'message'):
|
|
58
|
+
return []
|
|
59
|
+
|
|
60
|
+
ss = so.data.decode('utf-8')
|
|
61
|
+
if ss == '[DONE]':
|
|
62
|
+
return [None]
|
|
63
|
+
|
|
64
|
+
sj = json.loads(ss) # ChatCompletionChunk
|
|
65
|
+
|
|
66
|
+
check.state(sj['object'] == 'chat.completion.chunk')
|
|
67
|
+
|
|
68
|
+
ccc = msh.unmarshal(sj, pt.ChatCompletionChunk)
|
|
69
|
+
|
|
70
|
+
# FIXME: stop reason
|
|
71
|
+
if not ccc.choices:
|
|
72
|
+
return []
|
|
73
|
+
|
|
74
|
+
if any(choice.finish_reason for choice in ccc.choices):
|
|
75
|
+
check.state(all(choice.finish_reason for choice in ccc.choices))
|
|
76
|
+
return [None]
|
|
77
|
+
|
|
78
|
+
return [AiChoicesDeltas([
|
|
79
|
+
AiChoiceDeltas([build_mc_ai_delta(choice.delta)])
|
|
80
|
+
for choice in ccc.choices
|
|
81
|
+
])]
|
|
82
|
+
|
|
57
83
|
READ_CHUNK_SIZE: ta.ClassVar[int] = -1
|
|
58
84
|
|
|
59
85
|
async def invoke(self, request: ChatChoicesStreamRequest) -> ChatChoicesStreamResponse:
|
|
60
|
-
# check.isinstance(request, ChatRequest)
|
|
61
|
-
|
|
62
86
|
rh = OpenaiChatRequestHandler(
|
|
63
87
|
request.v,
|
|
64
88
|
*[
|
|
@@ -78,7 +102,7 @@ class OpenaiChatChoicesStreamService:
|
|
|
78
102
|
raw_request = msh.marshal(rh.oai_request())
|
|
79
103
|
|
|
80
104
|
http_request = http.HttpRequest(
|
|
81
|
-
|
|
105
|
+
self.URL,
|
|
82
106
|
headers={
|
|
83
107
|
http.consts.HEADER_CONTENT_TYPE: http.consts.CONTENT_TYPE_JSON,
|
|
84
108
|
http.consts.HEADER_AUTH: http.consts.format_bearer_auth_header(check.not_none(self._api_key).reveal()),
|
|
@@ -86,50 +110,11 @@ class OpenaiChatChoicesStreamService:
|
|
|
86
110
|
data=json.dumps(raw_request).encode('utf-8'),
|
|
87
111
|
)
|
|
88
112
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
http_response
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
b = await http_response.stream.read1(self.READ_CHUNK_SIZE)
|
|
98
|
-
for l in db.feed(b):
|
|
99
|
-
if isinstance(l, DelimitingBuffer.Incomplete):
|
|
100
|
-
# FIXME: handle
|
|
101
|
-
return []
|
|
102
|
-
|
|
103
|
-
# FIXME: https://platform.openai.com/docs/guides/function-calling?api-mode=responses#streaming
|
|
104
|
-
for so in sd.process_line(l):
|
|
105
|
-
if isinstance(so, sse.SseEvent) and so.type == b'message':
|
|
106
|
-
ss = so.data.decode('utf-8')
|
|
107
|
-
if ss == '[DONE]':
|
|
108
|
-
return []
|
|
109
|
-
|
|
110
|
-
sj = json.loads(ss) # ChatCompletionChunk
|
|
111
|
-
|
|
112
|
-
check.state(sj['object'] == 'chat.completion.chunk')
|
|
113
|
-
|
|
114
|
-
ccc = msh.unmarshal(sj, pt.ChatCompletionChunk)
|
|
115
|
-
|
|
116
|
-
# FIXME: stop reason
|
|
117
|
-
if not ccc.choices:
|
|
118
|
-
continue
|
|
119
|
-
|
|
120
|
-
if any(choice.finish_reason for choice in ccc.choices):
|
|
121
|
-
check.state(all(choice.finish_reason for choice in ccc.choices))
|
|
122
|
-
break
|
|
123
|
-
|
|
124
|
-
await sink.emit(AiChoicesDeltas([
|
|
125
|
-
AiChoiceDeltas([build_mc_ai_delta(choice.delta)])
|
|
126
|
-
for choice in ccc.choices
|
|
127
|
-
]))
|
|
128
|
-
|
|
129
|
-
if not b:
|
|
130
|
-
return []
|
|
131
|
-
|
|
132
|
-
# raw_response = json.loads(check.not_none(http_response.data).decode('utf-8'))
|
|
133
|
-
# return rh.build_response(raw_response)
|
|
134
|
-
|
|
135
|
-
return await new_stream_response(rs, inner)
|
|
113
|
+
return await BytesHttpStreamResponseBuilder(
|
|
114
|
+
self._http_client,
|
|
115
|
+
lambda http_response: SimpleSseLinesHttpStreamResponseHandler(self._process_sse).as_lines().as_bytes(),
|
|
116
|
+
read_chunk_size=self.READ_CHUNK_SIZE,
|
|
117
|
+
).new_stream_response(
|
|
118
|
+
http_request,
|
|
119
|
+
request.options,
|
|
120
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TODO:
|
|
3
|
+
- better pipeline composition lol
|
|
4
|
+
"""
|
|
5
|
+
import typing as ta
|
|
6
|
+
|
|
7
|
+
from omlish import check
|
|
8
|
+
from omlish import dataclasses as dc
|
|
9
|
+
from omlish import lang
|
|
10
|
+
from omlish.http import all as http
|
|
11
|
+
from omlish.http import sse
|
|
12
|
+
from omlish.io.buffers import DelimitingBuffer
|
|
13
|
+
|
|
14
|
+
from ..resources import UseResources
|
|
15
|
+
from ..stream.services import StreamResponse
|
|
16
|
+
from ..stream.services import StreamResponseSink
|
|
17
|
+
from ..stream.services import new_stream_response
|
|
18
|
+
from ..types import Option
|
|
19
|
+
from ..types import Output
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
##
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dc.dataclass()
|
|
26
|
+
@dc.extra_class_params(default_repr_fn=lang.opt_repr)
|
|
27
|
+
class HttpStreamResponseError(Exception):
|
|
28
|
+
response: http.BaseHttpResponse
|
|
29
|
+
|
|
30
|
+
data: bytes | None = None
|
|
31
|
+
data_exception: Exception | None = None
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
async def from_response(cls, response: http.AsyncStreamHttpResponse) -> 'HttpStreamResponseError':
|
|
35
|
+
data: bytes | None = None
|
|
36
|
+
data_exception: Exception | None = None
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
data = await response.stream.readall()
|
|
40
|
+
except Exception as de: # noqa
|
|
41
|
+
data_exception = de
|
|
42
|
+
|
|
43
|
+
return HttpStreamResponseError(
|
|
44
|
+
response,
|
|
45
|
+
data=data,
|
|
46
|
+
data_exception=data_exception,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class HttpStreamResponseHandler(lang.Abstract):
|
|
54
|
+
def start(self) -> ta.Sequence[Output]:
|
|
55
|
+
return ()
|
|
56
|
+
|
|
57
|
+
def finish(self) -> ta.Sequence[Output]:
|
|
58
|
+
return ()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
##
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class BytesHttpStreamResponseHandler(HttpStreamResponseHandler, lang.Abstract):
|
|
65
|
+
def process_bytes(self, data: bytes) -> ta.Iterable:
|
|
66
|
+
return ()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class BytesHttpStreamResponseBuilder:
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
http_client: http.AsyncHttpClient | None,
|
|
73
|
+
handling: ta.Callable[[http.AsyncStreamHttpResponse], BytesHttpStreamResponseHandler],
|
|
74
|
+
*,
|
|
75
|
+
read_chunk_size: int = -1,
|
|
76
|
+
) -> None:
|
|
77
|
+
super().__init__()
|
|
78
|
+
|
|
79
|
+
self._http_client = http_client
|
|
80
|
+
self._handling = handling
|
|
81
|
+
self._read_chunk_size = read_chunk_size
|
|
82
|
+
|
|
83
|
+
async def new_stream_response(
|
|
84
|
+
self,
|
|
85
|
+
http_request: http.HttpRequest,
|
|
86
|
+
options: ta.Sequence[Option],
|
|
87
|
+
) -> StreamResponse:
|
|
88
|
+
async with UseResources.or_new(options) as rs:
|
|
89
|
+
http_client = await rs.enter_async_context(http.manage_async_client(self._http_client))
|
|
90
|
+
http_response = await rs.enter_async_context(await http_client.stream_request(http_request))
|
|
91
|
+
|
|
92
|
+
if http_response.status != 200:
|
|
93
|
+
raise await HttpStreamResponseError.from_response(http_response)
|
|
94
|
+
|
|
95
|
+
handler = self._handling(http_response)
|
|
96
|
+
|
|
97
|
+
async def inner(sink: StreamResponseSink) -> ta.Sequence | None:
|
|
98
|
+
while True:
|
|
99
|
+
b = await http_response.stream.read1(self._read_chunk_size)
|
|
100
|
+
|
|
101
|
+
for v in handler.process_bytes(b):
|
|
102
|
+
if v is None:
|
|
103
|
+
break
|
|
104
|
+
|
|
105
|
+
await sink.emit(v)
|
|
106
|
+
|
|
107
|
+
if not b:
|
|
108
|
+
break
|
|
109
|
+
|
|
110
|
+
return handler.finish()
|
|
111
|
+
|
|
112
|
+
return await new_stream_response(
|
|
113
|
+
rs,
|
|
114
|
+
inner,
|
|
115
|
+
handler.start(),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
##
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class LinesHttpStreamResponseHandler(HttpStreamResponseHandler, lang.Abstract):
|
|
123
|
+
def process_line(self, line: bytes) -> ta.Iterable:
|
|
124
|
+
return ()
|
|
125
|
+
|
|
126
|
+
def as_bytes(self) -> BytesHttpStreamResponseHandler:
|
|
127
|
+
return LinesBytesHttpStreamResponseHandler(self)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class LinesBytesHttpStreamResponseHandler(BytesHttpStreamResponseHandler):
|
|
131
|
+
def __init__(self, handler: LinesHttpStreamResponseHandler) -> None:
|
|
132
|
+
super().__init__()
|
|
133
|
+
|
|
134
|
+
self._handler = handler
|
|
135
|
+
|
|
136
|
+
self._db = DelimitingBuffer([b'\r', b'\n', b'\r\n'])
|
|
137
|
+
|
|
138
|
+
def start(self) -> ta.Sequence[Output]:
|
|
139
|
+
return self._handler.start()
|
|
140
|
+
|
|
141
|
+
def process_bytes(self, data: bytes) -> ta.Iterable:
|
|
142
|
+
for o in self._db.feed(data):
|
|
143
|
+
if isinstance(o, bytes):
|
|
144
|
+
yield from self._handler.process_line(o)
|
|
145
|
+
|
|
146
|
+
else:
|
|
147
|
+
raise TypeError(o)
|
|
148
|
+
|
|
149
|
+
def finish(self) -> ta.Sequence[Output]:
|
|
150
|
+
check.state(self._db.is_closed)
|
|
151
|
+
|
|
152
|
+
return self._handler.finish()
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
##
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class SseHttpStreamResponseHandler(HttpStreamResponseHandler, lang.Abstract):
|
|
159
|
+
def process_sse(self, so: sse.SseDecoderOutput) -> ta.Iterable:
|
|
160
|
+
return ()
|
|
161
|
+
|
|
162
|
+
def as_lines(self) -> LinesHttpStreamResponseHandler:
|
|
163
|
+
return SseLinesHttpStreamResponseHandler(self)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class SseLinesHttpStreamResponseHandler(LinesHttpStreamResponseHandler):
|
|
167
|
+
def __init__(self, handler: SseHttpStreamResponseHandler) -> None:
|
|
168
|
+
super().__init__()
|
|
169
|
+
|
|
170
|
+
self._handler = handler
|
|
171
|
+
|
|
172
|
+
self._sd = sse.SseDecoder()
|
|
173
|
+
|
|
174
|
+
def start(self) -> ta.Sequence[Output]:
|
|
175
|
+
return self._handler.start()
|
|
176
|
+
|
|
177
|
+
def process_line(self, line: bytes) -> ta.Iterable:
|
|
178
|
+
for so in self._sd.process_line(line):
|
|
179
|
+
yield from self._handler.process_sse(so)
|
|
180
|
+
|
|
181
|
+
def finish(self) -> ta.Sequence[Output]:
|
|
182
|
+
return self._handler.finish()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
#
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class SimpleSseLinesHttpStreamResponseHandler(SseHttpStreamResponseHandler):
|
|
189
|
+
def __init__(self, fn: ta.Callable[[sse.SseDecoderOutput], ta.Iterable]) -> None:
|
|
190
|
+
super().__init__()
|
|
191
|
+
|
|
192
|
+
self._fn = fn
|
|
193
|
+
|
|
194
|
+
def process_sse(self, so: sse.SseDecoderOutput) -> ta.Iterable:
|
|
195
|
+
return self._fn(so)
|
ommlds/minichain/resources.py
CHANGED
|
@@ -33,6 +33,10 @@ class ResourcesRefNotRegisteredError(Exception):
|
|
|
33
33
|
|
|
34
34
|
@ta.final
|
|
35
35
|
class Resources(lang.Final, lang.NotPicklable):
|
|
36
|
+
"""
|
|
37
|
+
Essentially a reference-tracked AsyncContextManager.
|
|
38
|
+
"""
|
|
39
|
+
|
|
36
40
|
def __init__(
|
|
37
41
|
self,
|
|
38
42
|
*,
|
|
@@ -80,10 +84,14 @@ class Resources(lang.Final, lang.NotPicklable):
|
|
|
80
84
|
@contextlib.asynccontextmanager
|
|
81
85
|
async def inner():
|
|
82
86
|
init_ref = Resources._InitRef()
|
|
87
|
+
|
|
83
88
|
res = Resources(init_ref=init_ref, **kwargs)
|
|
89
|
+
|
|
84
90
|
await res.init()
|
|
91
|
+
|
|
85
92
|
try:
|
|
86
93
|
yield res
|
|
94
|
+
|
|
87
95
|
finally:
|
|
88
96
|
await res.remove_ref(init_ref)
|
|
89
97
|
|
|
@@ -94,6 +102,7 @@ class Resources(lang.Final, lang.NotPicklable):
|
|
|
94
102
|
def add_ref(self, ref: ResourcesRef) -> None:
|
|
95
103
|
check.isinstance(ref, ResourcesRef)
|
|
96
104
|
check.state(not self._closed)
|
|
105
|
+
|
|
97
106
|
self._refs.add(ref)
|
|
98
107
|
|
|
99
108
|
def has_ref(self, ref: ResourcesRef) -> bool:
|
|
@@ -101,10 +110,13 @@ class Resources(lang.Final, lang.NotPicklable):
|
|
|
101
110
|
|
|
102
111
|
async def remove_ref(self, ref: ResourcesRef) -> None:
|
|
103
112
|
check.isinstance(ref, ResourcesRef)
|
|
113
|
+
|
|
104
114
|
try:
|
|
105
115
|
self._refs.remove(ref)
|
|
116
|
+
|
|
106
117
|
except KeyError:
|
|
107
118
|
raise ResourcesRefNotRegisteredError(ref) from None
|
|
119
|
+
|
|
108
120
|
if not self._no_autoclose and not self._refs:
|
|
109
121
|
await self.aclose()
|
|
110
122
|
|
|
@@ -112,10 +124,12 @@ class Resources(lang.Final, lang.NotPicklable):
|
|
|
112
124
|
|
|
113
125
|
def enter_context(self, cm: ta.ContextManager[T]) -> T:
|
|
114
126
|
check.state(not self._closed)
|
|
127
|
+
|
|
115
128
|
return self._aes.enter_context(cm)
|
|
116
129
|
|
|
117
130
|
async def enter_async_context(self, cm: ta.AsyncContextManager[T]) -> T:
|
|
118
131
|
check.state(not self._closed)
|
|
132
|
+
|
|
119
133
|
return await self._aes.enter_async_context(cm)
|
|
120
134
|
|
|
121
135
|
#
|
|
@@ -150,7 +164,11 @@ class Resources(lang.Final, lang.NotPicklable):
|
|
|
150
164
|
class ResourceManaged(ResourcesRef, lang.Final, lang.NotPicklable, ta.Generic[T]):
|
|
151
165
|
"""
|
|
152
166
|
A class to 'handoff' a ref to a `Resources`, allowing the `Resources` to temporarily survive being passed from
|
|
153
|
-
instantiation within a callee
|
|
167
|
+
instantiation within a callee.
|
|
168
|
+
|
|
169
|
+
This class wraps an arbitrary value, likely an object referencing resources managed by the `Resources`, which is
|
|
170
|
+
accessed by `__aenter__`'ing. However, as the point of this class is handoff of a `Resources`, not necessarily some
|
|
171
|
+
arbitrary value, the value needn't necessarily be related to the `Resources`, or may even be `None`.
|
|
154
172
|
|
|
155
173
|
The ref to the `Resources` is allocated in the ctor, so the contract is that an instance of this must be immediately
|
|
156
174
|
`__aenter__`'d before doing anything else with the return value of the call. Failure to do so leaks the `Resources`.
|
|
@@ -172,11 +190,13 @@ class ResourceManaged(ResourcesRef, lang.Final, lang.NotPicklable, ta.Generic[T]
|
|
|
172
190
|
async def __aenter__(self) -> T:
|
|
173
191
|
check.state(self.__state == 'new')
|
|
174
192
|
self.__state = 'entered'
|
|
193
|
+
|
|
175
194
|
return self.__v
|
|
176
195
|
|
|
177
196
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
178
197
|
check.state(self.__state == 'entered')
|
|
179
198
|
self.__state = 'exited'
|
|
199
|
+
|
|
180
200
|
await self.__resources.remove_ref(self)
|
|
181
201
|
|
|
182
202
|
def __del__(self) -> None:
|
|
@@ -203,6 +223,7 @@ class UseResources(tv.UniqueScalarTypedValue[Resources], ResourcesOption, lang.F
|
|
|
203
223
|
if (ur := tv.as_collection(options).get(UseResources)) is not None:
|
|
204
224
|
async with ResourceManaged(ur.v, ur.v) as rs:
|
|
205
225
|
yield rs
|
|
226
|
+
|
|
206
227
|
else:
|
|
207
228
|
async with Resources.new() as rs:
|
|
208
229
|
yield rs
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TODO:
|
|
3
|
+
- new_stream_response carry TypeAlias __orig_type__ somehow
|
|
4
|
+
"""
|
|
1
5
|
import abc
|
|
2
6
|
import itertools
|
|
3
7
|
import types
|
|
@@ -104,17 +108,23 @@ class _StreamServiceResponse(StreamResponseIterator[V, OutputT]):
|
|
|
104
108
|
return _StreamServiceResponse._Emit(self._ssr, item)
|
|
105
109
|
|
|
106
110
|
_state: ta.Literal['new', 'running', 'closed'] = 'new'
|
|
111
|
+
|
|
107
112
|
_sink: _Sink[V]
|
|
108
|
-
|
|
113
|
+
|
|
109
114
|
_cr: ta.Any
|
|
115
|
+
_a: ta.Any
|
|
116
|
+
_g: ta.Any
|
|
110
117
|
|
|
111
118
|
async def __aenter__(self) -> ta.Self:
|
|
112
119
|
check.state(self._state == 'new')
|
|
113
120
|
self._state = 'running'
|
|
121
|
+
|
|
114
122
|
self._sink = _StreamServiceResponse._Sink(self)
|
|
123
|
+
|
|
115
124
|
self._cr = self._fn(self._sink)
|
|
116
125
|
self._a = self._cr.__await__()
|
|
117
126
|
self._g = iter(self._a)
|
|
127
|
+
|
|
118
128
|
return self
|
|
119
129
|
|
|
120
130
|
@types.coroutine
|
|
@@ -123,8 +133,10 @@ class _StreamServiceResponse(StreamResponseIterator[V, OutputT]):
|
|
|
123
133
|
self._state = 'closed'
|
|
124
134
|
if old_state != 'running':
|
|
125
135
|
return
|
|
136
|
+
|
|
126
137
|
if self._cr.cr_running or self._cr.cr_suspended:
|
|
127
138
|
cex = StreamServiceCancelledError()
|
|
139
|
+
|
|
128
140
|
i = None
|
|
129
141
|
for n in itertools.count():
|
|
130
142
|
try:
|
|
@@ -132,13 +144,18 @@ class _StreamServiceResponse(StreamResponseIterator[V, OutputT]):
|
|
|
132
144
|
x = self._g.throw(cex)
|
|
133
145
|
else:
|
|
134
146
|
x = self._g.send(i)
|
|
147
|
+
|
|
135
148
|
except StreamServiceCancelledError as cex2:
|
|
136
149
|
if cex2 is cex:
|
|
137
150
|
break
|
|
151
|
+
|
|
138
152
|
raise
|
|
153
|
+
|
|
139
154
|
i = yield x
|
|
155
|
+
|
|
140
156
|
if self._cr.cr_running:
|
|
141
157
|
raise RuntimeError(f'Coroutine {self._cr!r} not terminated')
|
|
158
|
+
|
|
142
159
|
if self._g is not self._a:
|
|
143
160
|
self._g.close()
|
|
144
161
|
self._a.close()
|
|
@@ -156,15 +173,18 @@ class _StreamServiceResponse(StreamResponseIterator[V, OutputT]):
|
|
|
156
173
|
@types.coroutine
|
|
157
174
|
def _anext(self):
|
|
158
175
|
check.state(self._state == 'running')
|
|
176
|
+
|
|
159
177
|
i = None
|
|
160
178
|
while True:
|
|
161
179
|
try:
|
|
162
180
|
x = self._g.send(i)
|
|
181
|
+
|
|
163
182
|
except StopIteration as e:
|
|
164
183
|
if e.value is not None:
|
|
165
184
|
self._outputs = tv.TypedValues(*check.isinstance(e.value, ta.Sequence))
|
|
166
185
|
else:
|
|
167
186
|
self._outputs = tv.TypedValues()
|
|
187
|
+
|
|
168
188
|
raise StopAsyncIteration from None
|
|
169
189
|
|
|
170
190
|
if isinstance(x, _StreamServiceResponse._Emit) and x.ssr is self:
|
|
@@ -200,11 +220,14 @@ async def new_stream_response(
|
|
|
200
220
|
ssr = _StreamServiceResponse(fn)
|
|
201
221
|
|
|
202
222
|
v = rs.new_managed(await rs.enter_async_context(ssr))
|
|
223
|
+
|
|
203
224
|
try:
|
|
204
225
|
return StreamResponse(v, outputs or [])
|
|
226
|
+
|
|
205
227
|
except BaseException: # noqa
|
|
206
228
|
# The StreamResponse ctor can raise - for example in `_tv_field_coercer` - in which case we need to clean up the
|
|
207
229
|
# resources ref we have already allocated before reraising.
|
|
208
230
|
async with v:
|
|
209
231
|
pass
|
|
232
|
+
|
|
210
233
|
raise
|