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.
Files changed (39) hide show
  1. ommlds/.omlish-manifests.json +14 -7
  2. ommlds/backends/anthropic/protocol/_dataclasses.py +16 -16
  3. ommlds/backends/cerebras/_dataclasses.py +42 -42
  4. ommlds/backends/google/protocol/_dataclasses.py +64 -64
  5. ommlds/backends/groq/_dataclasses.py +36 -36
  6. ommlds/backends/ollama/_dataclasses.py +28 -28
  7. ommlds/backends/openai/protocol/_dataclasses.py +88 -88
  8. ommlds/backends/tavily/_dataclasses.py +16 -16
  9. ommlds/cli/_dataclasses.py +64 -114
  10. ommlds/cli/backends/inject.py +20 -0
  11. ommlds/cli/backends/meta.py +47 -0
  12. ommlds/cli/sessions/chat/drivers/ai/tools.py +3 -7
  13. ommlds/cli/sessions/chat/facades/commands/base.py +1 -1
  14. ommlds/minichain/__init__.py +38 -4
  15. ommlds/minichain/_dataclasses.py +452 -289
  16. ommlds/minichain/backends/impls/anthropic/stream.py +1 -1
  17. ommlds/minichain/backends/impls/cerebras/names.py +15 -0
  18. ommlds/minichain/backends/impls/cerebras/stream.py +39 -52
  19. ommlds/minichain/backends/impls/google/chat.py +11 -82
  20. ommlds/minichain/backends/impls/google/protocol.py +105 -0
  21. ommlds/minichain/backends/impls/google/stream.py +49 -132
  22. ommlds/minichain/backends/impls/groq/stream.py +40 -53
  23. ommlds/minichain/backends/impls/ollama/chat.py +1 -1
  24. ommlds/minichain/backends/impls/openai/format.py +1 -0
  25. ommlds/minichain/backends/impls/openai/stream.py +40 -55
  26. ommlds/minichain/http/__init__.py +0 -0
  27. ommlds/minichain/http/stream.py +195 -0
  28. ommlds/minichain/resources.py +22 -1
  29. ommlds/minichain/stream/services.py +24 -1
  30. ommlds/minichain/wrappers/firstinwins.py +1 -1
  31. ommlds/minichain/wrappers/instrument.py +1 -1
  32. ommlds/minichain/wrappers/retry.py +34 -13
  33. {ommlds-0.0.0.dev502.dist-info → ommlds-0.0.0.dev504.dist-info}/METADATA +4 -4
  34. {ommlds-0.0.0.dev502.dist-info → ommlds-0.0.0.dev504.dist-info}/RECORD +38 -36
  35. ommlds/minichain/stream/wrap.py +0 -62
  36. {ommlds-0.0.0.dev502.dist-info → ommlds-0.0.0.dev504.dist-info}/WHEEL +0 -0
  37. {ommlds-0.0.0.dev502.dist-info → ommlds-0.0.0.dev504.dist-info}/entry_points.txt +0 -0
  38. {ommlds-0.0.0.dev502.dist-info → ommlds-0.0.0.dev504.dist-info}/licenses/LICENSE +0 -0
  39. {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 ....resources import UseResources
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
- 'https://api.groq.com/openai/v1/chat/completions',
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
- async with UseResources.or_new(request.options) as rs:
82
- http_client = await rs.enter_async_context(http.manage_async_client(self._http_client))
83
- http_response = await rs.enter_async_context(await http_client.stream_request(http_request))
84
-
85
- async def inner(sink: StreamResponseSink[AiChoicesDeltas]) -> ta.Sequence[ChatChoicesOutputs]:
86
- db = DelimitingBuffer([b'\r', b'\n', b'\r\n'])
87
- sd = sse.SseDecoder()
88
- while True:
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
- return []
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)
@@ -145,6 +145,7 @@ def build_mc_ai_delta(delta: pt.ChatCompletionChunkChoiceDelta) -> AiDelta:
145
145
  )
146
146
 
147
147
  else:
148
+ # FIXME: no
148
149
  return ContentAiDelta('')
149
150
 
150
151
 
@@ -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
- 'https://api.openai.com/v1/chat/completions',
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
- async with UseResources.or_new(request.options) as rs:
90
- http_client = await rs.enter_async_context(http.manage_async_client(self._http_client))
91
- http_response = await rs.enter_async_context(await http_client.stream_request(http_request))
92
-
93
- async def inner(sink: StreamResponseSink[AiChoicesDeltas]) -> ta.Sequence[ChatChoicesOutputs]:
94
- db = DelimitingBuffer([b'\r', b'\n', b'\r\n'])
95
- sd = sse.SseDecoder()
96
- while True:
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)
@@ -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 to being `__aenter__`'d in the caller.
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
- _a: ta.Any
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
@@ -34,7 +34,7 @@ AnyFirstInWinsService: ta.TypeAlias = ta.Union[
34
34
  ##
35
35
 
36
36
 
37
- @dc.dataclass(frozen=True)
37
+ @dc.dataclass()
38
38
  class FirstInWinsServiceCancelledError(Exception):
39
39
  e: BaseException
40
40
 
@@ -1,6 +1,6 @@
1
1
  """
2
2
  TODO:
3
- - final sream outputs
3
+ - final stream outputs
4
4
  """
5
5
  import datetime
6
6
  import typing as ta