dex-python-sdk 0.2.6__tar.gz → 0.2.8__tar.gz
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.
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/PKG-INFO +21 -2
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/README.md +20 -1
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/__init__.py +8 -1
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/_async_worker_dispatcher.py +68 -28
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/_invocation_context.py +48 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/context.py +13 -0
- dex_python_sdk-0.2.8/dex/stream.py +408 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/pyproject.toml +1 -1
- dex_python_sdk-0.2.6/dex/stream.py +0 -106
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/LEGACY_NOTICES.md +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/LICENSE +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/_async_value_hydrator.py +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/_async_worker_service.py +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/_grpc_errors.py +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/_native.pyi +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/_utils.py +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/_value_hydrator.py +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/_value_mapper.py +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/_worker_dispatcher.py +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/_worker_service.py +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/async_client.py +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/async_worker.py +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/attribute.py +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/blob_cache.py +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/channel.py +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/client.py +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/client_options.py +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/codec.py +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/condition.py +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/dexpb/__init__.py +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/dexpb/dex_pb2.py +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/dexpb/dex_pb2.pyi +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/dexpb/dex_pb2_grpc.py +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/flow.py +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/flow_config.py +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/flow_info.py +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/flow_options.py +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/flow_result.py +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/py.typed +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/runtime_errors.py +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/step.py +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/step_execution.py +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/subflow.py +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/timer.py +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/wait.py +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/worker.py +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/dex/worker_options.py +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/sdk-rust/Cargo.lock +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/sdk-rust/Cargo.toml +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/sdk-rust/crates/dex-blob-cache/Cargo.toml +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/sdk-rust/crates/dex-blob-cache/LICENSE +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/sdk-rust/crates/dex-blob-cache/src/config.rs +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/sdk-rust/crates/dex-blob-cache/src/entry.rs +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/sdk-rust/crates/dex-blob-cache/src/error.rs +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/sdk-rust/crates/dex-blob-cache/src/format.rs +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/sdk-rust/crates/dex-blob-cache/src/lib.rs +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/sdk-rust/crates/dex-blob-cache/src/policy.rs +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/sdk-rust/crates/dex-blob-cache/src/store.rs +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/sdk-rust/crates/dex-blob-cache/tests/blob_cache_integration.rs +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/sdk-rust/crates/dex-blob-cache-python/Cargo.toml +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/sdk-rust/crates/dex-blob-cache-python/LICENSE +0 -0
- {dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/sdk-rust/crates/dex-blob-cache-python/src/lib.rs +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dex-python-sdk
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
4
4
|
Requires-Dist: grpcio>=1.83.0
|
|
5
5
|
Requires-Dist: grpcio-status>=1.83.0
|
|
6
6
|
Requires-Dist: protobuf>=7.35.1
|
|
@@ -173,11 +173,30 @@ Worker output queue:
|
|
|
173
173
|
async def execute(
|
|
174
174
|
self, context: dex.AsyncContext, input: str
|
|
175
175
|
) -> dex.StepDecision:
|
|
176
|
-
progress.
|
|
176
|
+
writer = progress.buffered_text(context)
|
|
177
|
+
writer.write("started")
|
|
177
178
|
await context.heartbeat({"offset": 10})
|
|
178
179
|
return dex.graceful_complete(input)
|
|
179
180
|
```
|
|
180
181
|
|
|
182
|
+
The async buffered writer has a synchronous `write` method, so `writer.write`
|
|
183
|
+
can be passed directly as an LLM delta callback. It flushes after one second, at
|
|
184
|
+
a soft 16 KiB UTF-8 threshold, or before the final result or error. It
|
|
185
|
+
concatenates chunks exactly and ignores empty chunks.
|
|
186
|
+
|
|
187
|
+
A synchronous generator uses the cooperative form because only yielded
|
|
188
|
+
`StepOutput` values can reach gRPC:
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
writer = progress.buffered_text(context)
|
|
192
|
+
yield from writer.write(delta)
|
|
193
|
+
yield from writer.flush()
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Its interval is checked by the next write, and the handler must explicitly
|
|
197
|
+
flush the tail. Retry does not restore either writer's unsent buffer or
|
|
198
|
+
deduplicate sent batches.
|
|
199
|
+
|
|
181
200
|
Call `heartbeat()` or `await context.heartbeat()` without a value to clear previous
|
|
182
201
|
details. Passing Python `None` persists a present null Value. On a later regular
|
|
183
202
|
attempt, use `context.has_last_heartbeat_value()` before decoding with
|
|
@@ -157,11 +157,30 @@ Worker output queue:
|
|
|
157
157
|
async def execute(
|
|
158
158
|
self, context: dex.AsyncContext, input: str
|
|
159
159
|
) -> dex.StepDecision:
|
|
160
|
-
progress.
|
|
160
|
+
writer = progress.buffered_text(context)
|
|
161
|
+
writer.write("started")
|
|
161
162
|
await context.heartbeat({"offset": 10})
|
|
162
163
|
return dex.graceful_complete(input)
|
|
163
164
|
```
|
|
164
165
|
|
|
166
|
+
The async buffered writer has a synchronous `write` method, so `writer.write`
|
|
167
|
+
can be passed directly as an LLM delta callback. It flushes after one second, at
|
|
168
|
+
a soft 16 KiB UTF-8 threshold, or before the final result or error. It
|
|
169
|
+
concatenates chunks exactly and ignores empty chunks.
|
|
170
|
+
|
|
171
|
+
A synchronous generator uses the cooperative form because only yielded
|
|
172
|
+
`StepOutput` values can reach gRPC:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
writer = progress.buffered_text(context)
|
|
176
|
+
yield from writer.write(delta)
|
|
177
|
+
yield from writer.flush()
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Its interval is checked by the next write, and the handler must explicitly
|
|
181
|
+
flush the tail. Retry does not restore either writer's unsent buffer or
|
|
182
|
+
deduplicate sent batches.
|
|
183
|
+
|
|
165
184
|
Call `heartbeat()` or `await context.heartbeat()` without a value to clear previous
|
|
166
185
|
details. Passing Python `None` persists a present null Value. On a later regular
|
|
167
186
|
attempt, use `context.has_last_heartbeat_value()` before decoding with
|
|
@@ -92,7 +92,12 @@ from dex.step import (
|
|
|
92
92
|
heartbeat,
|
|
93
93
|
)
|
|
94
94
|
from dex.step_execution import StepExecutionId, TimerId
|
|
95
|
-
from dex.stream import
|
|
95
|
+
from dex.stream import (
|
|
96
|
+
AsyncBufferedTextStream,
|
|
97
|
+
BufferedTextStream,
|
|
98
|
+
Stream,
|
|
99
|
+
StreamMessage,
|
|
100
|
+
)
|
|
96
101
|
from dex.subflow import SubFlow
|
|
97
102
|
from dex.timer import Timer
|
|
98
103
|
from dex.wait import Wait
|
|
@@ -112,9 +117,11 @@ __all__ = [
|
|
|
112
117
|
"AttributeMap",
|
|
113
118
|
"AsyncContext",
|
|
114
119
|
"AsyncClient",
|
|
120
|
+
"AsyncBufferedTextStream",
|
|
115
121
|
"AsyncWorker",
|
|
116
122
|
"BlobCache",
|
|
117
123
|
"BlobCacheConfig",
|
|
124
|
+
"BufferedTextStream",
|
|
118
125
|
"Channel",
|
|
119
126
|
"ChannelMap",
|
|
120
127
|
"Client",
|
|
@@ -29,6 +29,7 @@ class _AsyncStepOutputEmitter:
|
|
|
29
29
|
def __init__(self) -> None:
|
|
30
30
|
self.queue: asyncio.Queue[StepOutput] = asyncio.Queue()
|
|
31
31
|
self._is_closed = False
|
|
32
|
+
self._close_callbacks: list[Callable[[], None]] = []
|
|
32
33
|
|
|
33
34
|
def emit_nowait(self, output: StepOutput) -> None:
|
|
34
35
|
if self._is_closed:
|
|
@@ -44,7 +45,17 @@ class _AsyncStepOutputEmitter:
|
|
|
44
45
|
raise asyncio.CancelledError
|
|
45
46
|
|
|
46
47
|
def close(self) -> None:
|
|
48
|
+
if self._is_closed:
|
|
49
|
+
return
|
|
47
50
|
self._is_closed = True
|
|
51
|
+
for callback in self._close_callbacks:
|
|
52
|
+
callback()
|
|
53
|
+
|
|
54
|
+
def add_close_callback(self, callback: Callable[[], None]) -> None:
|
|
55
|
+
if self._is_closed:
|
|
56
|
+
callback()
|
|
57
|
+
return
|
|
58
|
+
self._close_callbacks.append(callback)
|
|
48
59
|
|
|
49
60
|
|
|
50
61
|
class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
@@ -93,18 +104,21 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
93
104
|
output_emitter=emitter,
|
|
94
105
|
)
|
|
95
106
|
input = self._values.decode(request.step_input, step.input_codec)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
raise InvalidStepResultError(
|
|
100
|
-
flow.name,
|
|
101
|
-
step.name,
|
|
102
|
-
"wait_for",
|
|
103
|
-
"synchronous generators require Worker",
|
|
104
|
-
)
|
|
105
|
-
if isawaitable(wait):
|
|
106
|
-
wait = await wait
|
|
107
|
+
response: pb.InvokeWaitForMethodResponse | None = None
|
|
108
|
+
failure: BaseException | None = None
|
|
109
|
+
cause: BaseException | None = None
|
|
107
110
|
try:
|
|
111
|
+
wait = step.step.wait_for(context, input)
|
|
112
|
+
if isgenerator(wait):
|
|
113
|
+
wait.close()
|
|
114
|
+
raise InvalidStepResultError(
|
|
115
|
+
flow.name,
|
|
116
|
+
step.name,
|
|
117
|
+
"wait_for",
|
|
118
|
+
"synchronous generators require Worker",
|
|
119
|
+
)
|
|
120
|
+
if isawaitable(wait):
|
|
121
|
+
wait = await wait
|
|
108
122
|
if not isinstance(wait, Wait):
|
|
109
123
|
raise TypeError("wait_for must return Wait")
|
|
110
124
|
response = pb.InvokeWaitForMethodResponse(
|
|
@@ -116,11 +130,22 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
116
130
|
waiting = self._map_wait(flow, wait)
|
|
117
131
|
if waiting is not None:
|
|
118
132
|
response.waiting_condition.CopyFrom(waiting)
|
|
119
|
-
|
|
133
|
+
except InvalidStepResultError as error:
|
|
134
|
+
failure = error
|
|
120
135
|
except (TypeError, ValueError) as error:
|
|
121
|
-
|
|
136
|
+
failure = InvalidStepResultError(
|
|
122
137
|
flow.name, step.name, "wait_for", str(error)
|
|
123
|
-
)
|
|
138
|
+
)
|
|
139
|
+
cause = error
|
|
140
|
+
except BaseException as error:
|
|
141
|
+
failure = error
|
|
142
|
+
combined = context._finalize_step_outputs(failure)
|
|
143
|
+
if combined is not None:
|
|
144
|
+
if cause is not None:
|
|
145
|
+
raise combined from cause
|
|
146
|
+
raise combined
|
|
147
|
+
assert response is not None
|
|
148
|
+
return response
|
|
124
149
|
|
|
125
150
|
async def invoke_execute( # type: ignore[override]
|
|
126
151
|
self,
|
|
@@ -166,31 +191,46 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
166
191
|
output_emitter=emitter,
|
|
167
192
|
)
|
|
168
193
|
input = self._values.decode(request.step_input, step.input_codec)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
raise InvalidStepResultError(
|
|
173
|
-
flow.name,
|
|
174
|
-
step.name,
|
|
175
|
-
"execute",
|
|
176
|
-
"synchronous generators require Worker",
|
|
177
|
-
)
|
|
178
|
-
if isawaitable(decision):
|
|
179
|
-
decision = await decision
|
|
194
|
+
response: pb.InvokeExecuteMethodResponse | None = None
|
|
195
|
+
failure: BaseException | None = None
|
|
196
|
+
cause: BaseException | None = None
|
|
180
197
|
try:
|
|
198
|
+
decision: Any = step.step.execute(context, input)
|
|
199
|
+
if isgenerator(decision):
|
|
200
|
+
decision.close()
|
|
201
|
+
raise InvalidStepResultError(
|
|
202
|
+
flow.name,
|
|
203
|
+
step.name,
|
|
204
|
+
"execute",
|
|
205
|
+
"synchronous generators require Worker",
|
|
206
|
+
)
|
|
207
|
+
if isawaitable(decision):
|
|
208
|
+
decision = await decision
|
|
181
209
|
if not isinstance(decision, StepDecision):
|
|
182
210
|
raise TypeError("execute must return StepDecision")
|
|
183
|
-
|
|
211
|
+
response = pb.InvokeExecuteMethodResponse(
|
|
184
212
|
step_decision=self._map_decision(flow, decision),
|
|
185
213
|
upsert_attributes=list(context.attribute_writes.values()),
|
|
186
214
|
record_events=context.events,
|
|
187
215
|
upsert_step_exe_locals=list(context.local_writes.values()),
|
|
188
216
|
publish_to_channel=context.publications,
|
|
189
217
|
)
|
|
218
|
+
except InvalidStepResultError as error:
|
|
219
|
+
failure = error
|
|
190
220
|
except (TypeError, ValueError) as error:
|
|
191
|
-
|
|
221
|
+
failure = InvalidStepResultError(
|
|
192
222
|
flow.name, step.name, "execute", str(error)
|
|
193
|
-
)
|
|
223
|
+
)
|
|
224
|
+
cause = error
|
|
225
|
+
except BaseException as error:
|
|
226
|
+
failure = error
|
|
227
|
+
combined = context._finalize_step_outputs(failure)
|
|
228
|
+
if combined is not None:
|
|
229
|
+
if cause is not None:
|
|
230
|
+
raise combined from cause
|
|
231
|
+
raise combined
|
|
232
|
+
assert response is not None
|
|
233
|
+
return response
|
|
194
234
|
|
|
195
235
|
async def _invoke_timeout_handler_async(
|
|
196
236
|
self,
|
|
@@ -46,6 +46,14 @@ class _StepOutputEmitter(Protocol):
|
|
|
46
46
|
|
|
47
47
|
async def emit(self, output: StepOutput) -> None: ...
|
|
48
48
|
|
|
49
|
+
def add_close_callback(self, callback: Callable[[], None]) -> None: ...
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class _StepOutputFinalizer(Protocol):
|
|
53
|
+
def _finalize_step_output(self) -> None: ...
|
|
54
|
+
|
|
55
|
+
def _cancel_step_output(self) -> None: ...
|
|
56
|
+
|
|
49
57
|
|
|
50
58
|
class InvocationContext:
|
|
51
59
|
def __init__(
|
|
@@ -76,6 +84,8 @@ class InvocationContext:
|
|
|
76
84
|
self.events: list[pb.KV] = []
|
|
77
85
|
self.publications: list[pb.ChannelMessage] = []
|
|
78
86
|
self._event_names: set[str] = set()
|
|
87
|
+
self._step_output_finalizers: list[_StepOutputFinalizer] = []
|
|
88
|
+
self._step_outputs_finalized = False
|
|
79
89
|
|
|
80
90
|
@property
|
|
81
91
|
def flow_id(self) -> str:
|
|
@@ -221,6 +231,44 @@ class InvocationContext:
|
|
|
221
231
|
self._output_emitter.emit_nowait(output)
|
|
222
232
|
return None
|
|
223
233
|
|
|
234
|
+
def _prepare_buffered_stream(self, definition: Stream[object]) -> bool:
|
|
235
|
+
if self._method not in (InvocationMethod.WAIT_FOR, InvocationMethod.EXECUTE):
|
|
236
|
+
raise ValueError("Buffered Streams require a Step Context")
|
|
237
|
+
self._require_registered(definition)
|
|
238
|
+
return self._output_emitter is not None
|
|
239
|
+
|
|
240
|
+
def _register_step_output_finalizer(
|
|
241
|
+
self,
|
|
242
|
+
finalizer: _StepOutputFinalizer,
|
|
243
|
+
) -> None:
|
|
244
|
+
if self._step_outputs_finalized or self._output_emitter is None:
|
|
245
|
+
raise ValueError(
|
|
246
|
+
"Buffered Stream finalizers require an active async Step Context"
|
|
247
|
+
)
|
|
248
|
+
self._step_output_finalizers.append(finalizer)
|
|
249
|
+
self._output_emitter.add_close_callback(finalizer._cancel_step_output)
|
|
250
|
+
|
|
251
|
+
def _finalize_step_outputs(
|
|
252
|
+
self,
|
|
253
|
+
failure: BaseException | None = None,
|
|
254
|
+
) -> BaseException | None:
|
|
255
|
+
if self._step_outputs_finalized:
|
|
256
|
+
return failure
|
|
257
|
+
self._step_outputs_finalized = True
|
|
258
|
+
failures: list[BaseException] = [] if failure is None else [failure]
|
|
259
|
+
for finalizer in self._step_output_finalizers:
|
|
260
|
+
try:
|
|
261
|
+
finalizer._finalize_step_output()
|
|
262
|
+
except BaseException as error:
|
|
263
|
+
failures.append(error)
|
|
264
|
+
if not failures:
|
|
265
|
+
return None
|
|
266
|
+
if len(failures) == 1:
|
|
267
|
+
return failures[0]
|
|
268
|
+
return BaseExceptionGroup(
|
|
269
|
+
"Step handler and buffered Stream finalization failed", failures
|
|
270
|
+
)
|
|
271
|
+
|
|
224
272
|
def _get_attribute(
|
|
225
273
|
self,
|
|
226
274
|
definition: Attribute[ValueT] | AttributeMap[ValueT],
|
|
@@ -18,6 +18,12 @@ if TYPE_CHECKING:
|
|
|
18
18
|
from dex.step import StepOutput
|
|
19
19
|
from dex.stream import Stream
|
|
20
20
|
|
|
21
|
+
class _StepOutputFinalizer(Protocol):
|
|
22
|
+
def _finalize_step_output(self) -> None: ...
|
|
23
|
+
|
|
24
|
+
def _cancel_step_output(self) -> None: ...
|
|
25
|
+
|
|
26
|
+
|
|
21
27
|
ValueT = TypeVar("ValueT")
|
|
22
28
|
|
|
23
29
|
|
|
@@ -227,6 +233,13 @@ class Context(Protocol):
|
|
|
227
233
|
value: ValueT,
|
|
228
234
|
) -> StepOutput | None: ...
|
|
229
235
|
|
|
236
|
+
def _prepare_buffered_stream(self, definition: Stream[object]) -> bool: ...
|
|
237
|
+
|
|
238
|
+
def _register_step_output_finalizer(
|
|
239
|
+
self,
|
|
240
|
+
finalizer: _StepOutputFinalizer,
|
|
241
|
+
) -> None: ...
|
|
242
|
+
|
|
230
243
|
|
|
231
244
|
class AsyncContext(Context, Protocol):
|
|
232
245
|
"""Expose Context operations available to asynchronous Step handlers.
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Super Durable Source License 1.0.
|
|
4
|
+
# You may not use this file except in compliance with the License.
|
|
5
|
+
# See the LICENSE file in the repository root.
|
|
6
|
+
#
|
|
7
|
+
# SPDX-License-Identifier: LicenseRef-Super-Durable-1.0
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import time
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from datetime import datetime, timedelta
|
|
15
|
+
from typing import TYPE_CHECKING, Generic, TypeVar, cast, overload
|
|
16
|
+
|
|
17
|
+
from dex._utils import require_name
|
|
18
|
+
from dex.step import StepOutput
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from dex.context import AsyncContext, Context
|
|
22
|
+
|
|
23
|
+
ValueT = TypeVar("ValueT")
|
|
24
|
+
_DEFAULT_BUFFERED_TEXT_FLUSH_INTERVAL = timedelta(seconds=1)
|
|
25
|
+
_DEFAULT_BUFFERED_TEXT_MAX_BYTES = 16 * 1024
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class Stream(Generic[ValueT]):
|
|
30
|
+
"""Define a typed best-effort resumable Stream owned by one Flow type.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
name: Logical name unique within the Flow persistence schema.
|
|
34
|
+
value_type: Python type encoded for every message.
|
|
35
|
+
stream_capacity_bytes: Positive approximate budget shared by all Flow instances.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
name: str
|
|
39
|
+
value_type: type[ValueT]
|
|
40
|
+
stream_capacity_bytes: int
|
|
41
|
+
|
|
42
|
+
def __post_init__(self) -> None:
|
|
43
|
+
"""Validate the Stream definition after dataclass construction.
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
ValueError: If the name is empty or the byte budget is not positive.
|
|
47
|
+
TypeError: If the byte budget is not an integer.
|
|
48
|
+
"""
|
|
49
|
+
require_name(self.name)
|
|
50
|
+
if not isinstance(self.stream_capacity_bytes, int):
|
|
51
|
+
raise TypeError("Stream stream_capacity_bytes must be an integer")
|
|
52
|
+
if self.stream_capacity_bytes <= 0:
|
|
53
|
+
raise ValueError("Stream stream_capacity_bytes must be positive")
|
|
54
|
+
|
|
55
|
+
@overload
|
|
56
|
+
def write( # type: ignore[overload-overlap]
|
|
57
|
+
self,
|
|
58
|
+
context: AsyncContext,
|
|
59
|
+
value: ValueT,
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Enqueue one message from an asynchronous Step handler."""
|
|
62
|
+
...
|
|
63
|
+
|
|
64
|
+
@overload
|
|
65
|
+
def write(self, context: Context, value: ValueT) -> StepOutput:
|
|
66
|
+
"""Create one message output for a synchronous Step generator."""
|
|
67
|
+
...
|
|
68
|
+
|
|
69
|
+
def write(
|
|
70
|
+
self,
|
|
71
|
+
context: Context,
|
|
72
|
+
value: ValueT,
|
|
73
|
+
) -> StepOutput | None:
|
|
74
|
+
"""Emit one best-effort message from the current Step execution.
|
|
75
|
+
|
|
76
|
+
A synchronous generator must yield the returned StepOutput. An asynchronous
|
|
77
|
+
Step calls this method without ``await``; the message enters the invocation's
|
|
78
|
+
ordered output queue immediately. Neither form waits for Dex Stream Store
|
|
79
|
+
persistence. Calls may write the same Stream any number of times. RPC and
|
|
80
|
+
Flow-timeout Contexts reject Stream writes.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
context: Current synchronous or asynchronous Step Context.
|
|
84
|
+
value: Typed message to append.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
A StepOutput for a synchronous Context, or ``None`` for AsyncContext.
|
|
88
|
+
|
|
89
|
+
Raises:
|
|
90
|
+
ValueError: If the Stream is unregistered or the Context is not a Step.
|
|
91
|
+
ValueMappingError: If ``value`` cannot be encoded.
|
|
92
|
+
"""
|
|
93
|
+
return context._write_stream(self, value)
|
|
94
|
+
|
|
95
|
+
@overload
|
|
96
|
+
def buffered_text( # type: ignore[overload-overlap]
|
|
97
|
+
self: Stream[str],
|
|
98
|
+
context: AsyncContext,
|
|
99
|
+
*,
|
|
100
|
+
flush_interval: timedelta = _DEFAULT_BUFFERED_TEXT_FLUSH_INTERVAL,
|
|
101
|
+
max_buffered_bytes: int = _DEFAULT_BUFFERED_TEXT_MAX_BYTES,
|
|
102
|
+
) -> AsyncBufferedTextStream: ...
|
|
103
|
+
|
|
104
|
+
@overload
|
|
105
|
+
def buffered_text(
|
|
106
|
+
self: Stream[str],
|
|
107
|
+
context: Context,
|
|
108
|
+
*,
|
|
109
|
+
flush_interval: timedelta = _DEFAULT_BUFFERED_TEXT_FLUSH_INTERVAL,
|
|
110
|
+
max_buffered_bytes: int = _DEFAULT_BUFFERED_TEXT_MAX_BYTES,
|
|
111
|
+
) -> BufferedTextStream: ...
|
|
112
|
+
|
|
113
|
+
def buffered_text(
|
|
114
|
+
self: Stream[str],
|
|
115
|
+
context: Context,
|
|
116
|
+
*,
|
|
117
|
+
flush_interval: timedelta = _DEFAULT_BUFFERED_TEXT_FLUSH_INTERVAL,
|
|
118
|
+
max_buffered_bytes: int = _DEFAULT_BUFFERED_TEXT_MAX_BYTES,
|
|
119
|
+
) -> AsyncBufferedTextStream | BufferedTextStream:
|
|
120
|
+
"""Create an invocation-managed writer that batches text chunks.
|
|
121
|
+
|
|
122
|
+
The first non-empty chunk starts a one-shot timer. Async Step writers append their current
|
|
123
|
+
batch after ``flush_interval`` even without another chunk. Synchronous generator writers
|
|
124
|
+
check the interval only when ``write`` is called and require an explicit final ``flush``.
|
|
125
|
+
Both forms flush early after crossing the soft UTF-8 byte threshold. Chunks are never split
|
|
126
|
+
or modified.
|
|
127
|
+
|
|
128
|
+
AsyncWorker finalizes the writer before sending the invocation result or error. Its
|
|
129
|
+
synchronous ``write`` method can be passed directly as a text-delta callback. Neither form
|
|
130
|
+
waits for Dex Stream Store acknowledgement.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
context: Current asynchronous or synchronous Step Context.
|
|
134
|
+
flush_interval: Positive maximum async buffering interval. Defaults to one second.
|
|
135
|
+
max_buffered_bytes: Positive soft UTF-8 threshold. Defaults to 16 KiB.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
An async invocation-managed writer, or a cooperative synchronous writer.
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
TypeError: If this Stream does not carry ``str`` values or an option has the wrong type.
|
|
142
|
+
ValueError: If an option is not positive, the Stream is unregistered, or the Context is
|
|
143
|
+
not a Step invocation.
|
|
144
|
+
"""
|
|
145
|
+
if self.value_type is not str:
|
|
146
|
+
raise TypeError("Buffered Streams require Stream[str]")
|
|
147
|
+
_validate_buffered_text_options(flush_interval, max_buffered_bytes)
|
|
148
|
+
is_async = context._prepare_buffered_stream(cast(Stream[object], self))
|
|
149
|
+
if is_async:
|
|
150
|
+
writer = AsyncBufferedTextStream(
|
|
151
|
+
self,
|
|
152
|
+
context,
|
|
153
|
+
flush_interval,
|
|
154
|
+
max_buffered_bytes,
|
|
155
|
+
)
|
|
156
|
+
context._register_step_output_finalizer(writer)
|
|
157
|
+
return writer
|
|
158
|
+
return BufferedTextStream(
|
|
159
|
+
self,
|
|
160
|
+
context,
|
|
161
|
+
flush_interval,
|
|
162
|
+
max_buffered_bytes,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@dataclass(frozen=True)
|
|
167
|
+
class StreamMessage(Generic[ValueT]):
|
|
168
|
+
"""Describe one retained Stream message returned by a Client.
|
|
169
|
+
|
|
170
|
+
Attributes:
|
|
171
|
+
value: Decoded application message.
|
|
172
|
+
resume_token: Opaque token for the next read.
|
|
173
|
+
created_time: Server-assigned UTC creation time.
|
|
174
|
+
source: Informational client source or Step-generated ``#stepExecutionID``.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
value: ValueT
|
|
178
|
+
resume_token: str
|
|
179
|
+
created_time: datetime
|
|
180
|
+
source: str
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class AsyncBufferedTextStream:
|
|
184
|
+
"""Batch text chunks for an asynchronous Step invocation.
|
|
185
|
+
|
|
186
|
+
Create this writer with :meth:`Stream.buffered_text`. ``write`` is synchronous and returns
|
|
187
|
+
after local buffering or Worker-output enqueueing. AsyncWorker automatically stops its timer
|
|
188
|
+
and flushes remaining text before the invocation result or error.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
def __init__(
|
|
192
|
+
self,
|
|
193
|
+
stream: Stream[str],
|
|
194
|
+
context: Context,
|
|
195
|
+
flush_interval: timedelta,
|
|
196
|
+
max_buffered_bytes: int,
|
|
197
|
+
) -> None:
|
|
198
|
+
"""Initialize an invocation-managed writer from validated factory inputs.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
stream: Registered string Stream receiving each batch.
|
|
202
|
+
context: Current asynchronous Step Context.
|
|
203
|
+
flush_interval: Positive one-shot timer interval.
|
|
204
|
+
max_buffered_bytes: Positive soft UTF-8 batch threshold.
|
|
205
|
+
"""
|
|
206
|
+
self._stream = stream
|
|
207
|
+
self._context = context
|
|
208
|
+
self._flush_interval_seconds = flush_interval.total_seconds()
|
|
209
|
+
self._max_buffered_bytes = max_buffered_bytes
|
|
210
|
+
self._buffer: list[str] = []
|
|
211
|
+
self._buffered_bytes = 0
|
|
212
|
+
self._timer: asyncio.TimerHandle | None = None
|
|
213
|
+
self._timer_generation = 0
|
|
214
|
+
self._is_closed = False
|
|
215
|
+
self._terminal_error: BaseException | None = None
|
|
216
|
+
self._loop = asyncio.get_running_loop()
|
|
217
|
+
|
|
218
|
+
def write(self, chunk: str) -> None:
|
|
219
|
+
"""Append one text chunk and flush after reaching the soft size threshold.
|
|
220
|
+
|
|
221
|
+
Empty chunks are ignored. The first non-empty chunk starts the configured one-shot timer.
|
|
222
|
+
This method does not wait for the timer or Stream Store acknowledgement.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
chunk: Text to append without modification.
|
|
226
|
+
|
|
227
|
+
Raises:
|
|
228
|
+
TypeError: If ``chunk`` is not a string.
|
|
229
|
+
BaseException: The previously latched timer or Worker-output failure.
|
|
230
|
+
ValueError: If the invocation already finished.
|
|
231
|
+
"""
|
|
232
|
+
self._require_open()
|
|
233
|
+
if not isinstance(chunk, str):
|
|
234
|
+
raise TypeError("Buffered Stream chunks must be strings")
|
|
235
|
+
if not chunk:
|
|
236
|
+
return
|
|
237
|
+
was_empty = not self._buffer
|
|
238
|
+
self._buffer.append(chunk)
|
|
239
|
+
self._buffered_bytes += len(chunk.encode("utf-8"))
|
|
240
|
+
if was_empty:
|
|
241
|
+
self._start_timer()
|
|
242
|
+
if self._buffered_bytes >= self._max_buffered_bytes:
|
|
243
|
+
self._stop_timer()
|
|
244
|
+
self._flush_buffer()
|
|
245
|
+
|
|
246
|
+
def _finalize_step_output(self) -> None:
|
|
247
|
+
if self._is_closed:
|
|
248
|
+
if self._terminal_error is not None:
|
|
249
|
+
raise self._terminal_error
|
|
250
|
+
return
|
|
251
|
+
self._stop_timer()
|
|
252
|
+
try:
|
|
253
|
+
if self._terminal_error is not None:
|
|
254
|
+
raise self._terminal_error
|
|
255
|
+
self._flush_buffer()
|
|
256
|
+
finally:
|
|
257
|
+
self._is_closed = True
|
|
258
|
+
|
|
259
|
+
def _cancel_step_output(self) -> None:
|
|
260
|
+
self._stop_timer()
|
|
261
|
+
self._buffer.clear()
|
|
262
|
+
self._buffered_bytes = 0
|
|
263
|
+
self._is_closed = True
|
|
264
|
+
|
|
265
|
+
def _start_timer(self) -> None:
|
|
266
|
+
self._timer_generation += 1
|
|
267
|
+
generation = self._timer_generation
|
|
268
|
+
self._timer = self._loop.call_later(
|
|
269
|
+
self._flush_interval_seconds,
|
|
270
|
+
self._flush_from_timer,
|
|
271
|
+
generation,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
def _flush_from_timer(self, generation: int) -> None:
|
|
275
|
+
if (
|
|
276
|
+
self._is_closed
|
|
277
|
+
or self._terminal_error is not None
|
|
278
|
+
or generation != self._timer_generation
|
|
279
|
+
):
|
|
280
|
+
return
|
|
281
|
+
self._timer = None
|
|
282
|
+
try:
|
|
283
|
+
self._flush_buffer()
|
|
284
|
+
except BaseException as error:
|
|
285
|
+
self._terminal_error = error
|
|
286
|
+
|
|
287
|
+
def _stop_timer(self) -> None:
|
|
288
|
+
self._timer_generation += 1
|
|
289
|
+
if self._timer is not None:
|
|
290
|
+
self._timer.cancel()
|
|
291
|
+
self._timer = None
|
|
292
|
+
|
|
293
|
+
def _flush_buffer(self) -> None:
|
|
294
|
+
if not self._buffer:
|
|
295
|
+
return
|
|
296
|
+
value = "".join(self._buffer)
|
|
297
|
+
self._buffer.clear()
|
|
298
|
+
self._buffered_bytes = 0
|
|
299
|
+
try:
|
|
300
|
+
output = self._stream.write(self._context, value)
|
|
301
|
+
if output is not None:
|
|
302
|
+
raise RuntimeError(
|
|
303
|
+
"Async Buffered Stream produced a synchronous StepOutput"
|
|
304
|
+
)
|
|
305
|
+
except BaseException as error:
|
|
306
|
+
self._terminal_error = error
|
|
307
|
+
raise
|
|
308
|
+
|
|
309
|
+
def _require_open(self) -> None:
|
|
310
|
+
if self._terminal_error is not None:
|
|
311
|
+
raise self._terminal_error
|
|
312
|
+
if self._is_closed:
|
|
313
|
+
raise ValueError("Buffered Stream invocation has finished")
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class BufferedTextStream:
|
|
317
|
+
"""Batch text chunks for a synchronous Step generator.
|
|
318
|
+
|
|
319
|
+
Create this writer with :meth:`Stream.buffered_text`. Yield every output returned by ``write``
|
|
320
|
+
and ``flush``. The interval is cooperative: elapsed time is checked when another chunk arrives.
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
def __init__(
|
|
324
|
+
self,
|
|
325
|
+
stream: Stream[str],
|
|
326
|
+
context: Context,
|
|
327
|
+
flush_interval: timedelta,
|
|
328
|
+
max_buffered_bytes: int,
|
|
329
|
+
) -> None:
|
|
330
|
+
"""Initialize a cooperative writer from validated factory inputs.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
stream: Registered string Stream receiving each batch.
|
|
334
|
+
context: Current synchronous Step Context.
|
|
335
|
+
flush_interval: Positive cooperative flush interval.
|
|
336
|
+
max_buffered_bytes: Positive soft UTF-8 batch threshold.
|
|
337
|
+
"""
|
|
338
|
+
self._stream = stream
|
|
339
|
+
self._context = context
|
|
340
|
+
self._flush_interval_seconds = flush_interval.total_seconds()
|
|
341
|
+
self._max_buffered_bytes = max_buffered_bytes
|
|
342
|
+
self._buffer: list[str] = []
|
|
343
|
+
self._buffered_bytes = 0
|
|
344
|
+
self._started_at: float | None = None
|
|
345
|
+
|
|
346
|
+
def write(self, chunk: str) -> tuple[StepOutput, ...]:
|
|
347
|
+
"""Append one chunk and return an output when a threshold is reached.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
chunk: Text to append without modification.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
An empty tuple while buffering, or one StepOutput that the handler must yield.
|
|
354
|
+
|
|
355
|
+
Raises:
|
|
356
|
+
TypeError: If ``chunk`` is not a string.
|
|
357
|
+
ValueMappingError: If the combined text cannot be encoded.
|
|
358
|
+
"""
|
|
359
|
+
if not isinstance(chunk, str):
|
|
360
|
+
raise TypeError("Buffered Stream chunks must be strings")
|
|
361
|
+
if not chunk:
|
|
362
|
+
return ()
|
|
363
|
+
if self._started_at is None:
|
|
364
|
+
self._started_at = time.monotonic()
|
|
365
|
+
self._buffer.append(chunk)
|
|
366
|
+
self._buffered_bytes += len(chunk.encode("utf-8"))
|
|
367
|
+
has_elapsed = (
|
|
368
|
+
time.monotonic() - self._started_at >= self._flush_interval_seconds
|
|
369
|
+
)
|
|
370
|
+
if self._buffered_bytes < self._max_buffered_bytes and not has_elapsed:
|
|
371
|
+
return ()
|
|
372
|
+
return self.flush()
|
|
373
|
+
|
|
374
|
+
def flush(self) -> tuple[StepOutput, ...]:
|
|
375
|
+
"""Return the current batch as one StepOutput for the handler to yield.
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
An empty tuple for an empty buffer, or one Stream StepOutput.
|
|
379
|
+
|
|
380
|
+
Raises:
|
|
381
|
+
ValueMappingError: If the combined text cannot be encoded.
|
|
382
|
+
"""
|
|
383
|
+
if not self._buffer:
|
|
384
|
+
return ()
|
|
385
|
+
value = "".join(self._buffer)
|
|
386
|
+
self._buffer.clear()
|
|
387
|
+
self._buffered_bytes = 0
|
|
388
|
+
self._started_at = None
|
|
389
|
+
output = self._stream.write(self._context, value)
|
|
390
|
+
if output is None:
|
|
391
|
+
raise RuntimeError(
|
|
392
|
+
"Synchronous Buffered Stream did not produce a StepOutput"
|
|
393
|
+
)
|
|
394
|
+
return (output,)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _validate_buffered_text_options(
|
|
398
|
+
flush_interval: timedelta,
|
|
399
|
+
max_buffered_bytes: int,
|
|
400
|
+
) -> None:
|
|
401
|
+
if not isinstance(flush_interval, timedelta):
|
|
402
|
+
raise TypeError("Buffered Stream flush_interval must be timedelta")
|
|
403
|
+
if flush_interval <= timedelta(0):
|
|
404
|
+
raise ValueError("Buffered Stream flush_interval must be positive")
|
|
405
|
+
if not isinstance(max_buffered_bytes, int) or isinstance(max_buffered_bytes, bool):
|
|
406
|
+
raise TypeError("Buffered Stream max_buffered_bytes must be an integer")
|
|
407
|
+
if max_buffered_bytes <= 0:
|
|
408
|
+
raise ValueError("Buffered Stream max_buffered_bytes must be positive")
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2026 Super Durable, Inc.
|
|
2
|
-
#
|
|
3
|
-
# Licensed under the Super Durable Source License 1.0.
|
|
4
|
-
# You may not use this file except in compliance with the License.
|
|
5
|
-
# See the LICENSE file in the repository root.
|
|
6
|
-
#
|
|
7
|
-
# SPDX-License-Identifier: LicenseRef-Super-Durable-1.0
|
|
8
|
-
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
|
|
11
|
-
from dataclasses import dataclass
|
|
12
|
-
from datetime import datetime
|
|
13
|
-
from typing import TYPE_CHECKING, Generic, TypeVar, overload
|
|
14
|
-
|
|
15
|
-
from dex._utils import require_name
|
|
16
|
-
from dex.step import StepOutput
|
|
17
|
-
|
|
18
|
-
if TYPE_CHECKING:
|
|
19
|
-
from dex.context import AsyncContext, Context
|
|
20
|
-
|
|
21
|
-
ValueT = TypeVar("ValueT")
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@dataclass(frozen=True)
|
|
25
|
-
class Stream(Generic[ValueT]):
|
|
26
|
-
"""Define a typed best-effort resumable Stream owned by one Flow type.
|
|
27
|
-
|
|
28
|
-
Attributes:
|
|
29
|
-
name: Logical name unique within the Flow persistence schema.
|
|
30
|
-
value_type: Python type encoded for every message.
|
|
31
|
-
stream_capacity_bytes: Positive approximate budget shared by all Flow instances.
|
|
32
|
-
"""
|
|
33
|
-
|
|
34
|
-
name: str
|
|
35
|
-
value_type: type[ValueT]
|
|
36
|
-
stream_capacity_bytes: int
|
|
37
|
-
|
|
38
|
-
def __post_init__(self) -> None:
|
|
39
|
-
"""Validate the Stream definition after dataclass construction.
|
|
40
|
-
|
|
41
|
-
Raises:
|
|
42
|
-
ValueError: If the name is empty or the byte budget is not positive.
|
|
43
|
-
TypeError: If the byte budget is not an integer.
|
|
44
|
-
"""
|
|
45
|
-
require_name(self.name)
|
|
46
|
-
if not isinstance(self.stream_capacity_bytes, int):
|
|
47
|
-
raise TypeError("Stream stream_capacity_bytes must be an integer")
|
|
48
|
-
if self.stream_capacity_bytes <= 0:
|
|
49
|
-
raise ValueError("Stream stream_capacity_bytes must be positive")
|
|
50
|
-
|
|
51
|
-
@overload
|
|
52
|
-
def write( # type: ignore[overload-overlap]
|
|
53
|
-
self,
|
|
54
|
-
context: AsyncContext,
|
|
55
|
-
value: ValueT,
|
|
56
|
-
) -> None:
|
|
57
|
-
"""Enqueue one message from an asynchronous Step handler."""
|
|
58
|
-
...
|
|
59
|
-
|
|
60
|
-
@overload
|
|
61
|
-
def write(self, context: Context, value: ValueT) -> StepOutput:
|
|
62
|
-
"""Create one message output for a synchronous Step generator."""
|
|
63
|
-
...
|
|
64
|
-
|
|
65
|
-
def write(
|
|
66
|
-
self,
|
|
67
|
-
context: Context,
|
|
68
|
-
value: ValueT,
|
|
69
|
-
) -> StepOutput | None:
|
|
70
|
-
"""Emit one best-effort message from the current Step execution.
|
|
71
|
-
|
|
72
|
-
A synchronous generator must yield the returned StepOutput. An asynchronous
|
|
73
|
-
Step calls this method without ``await``; the message enters the invocation's
|
|
74
|
-
ordered output queue immediately. Neither form waits for Dex Stream Store
|
|
75
|
-
persistence. Calls may write the same Stream any number of times. RPC and
|
|
76
|
-
Flow-timeout Contexts reject Stream writes.
|
|
77
|
-
|
|
78
|
-
Args:
|
|
79
|
-
context: Current synchronous or asynchronous Step Context.
|
|
80
|
-
value: Typed message to append.
|
|
81
|
-
|
|
82
|
-
Returns:
|
|
83
|
-
A StepOutput for a synchronous Context, or ``None`` for AsyncContext.
|
|
84
|
-
|
|
85
|
-
Raises:
|
|
86
|
-
ValueError: If the Stream is unregistered or the Context is not a Step.
|
|
87
|
-
ValueMappingError: If ``value`` cannot be encoded.
|
|
88
|
-
"""
|
|
89
|
-
return context._write_stream(self, value)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
@dataclass(frozen=True)
|
|
93
|
-
class StreamMessage(Generic[ValueT]):
|
|
94
|
-
"""Describe one retained Stream message returned by a Client.
|
|
95
|
-
|
|
96
|
-
Attributes:
|
|
97
|
-
value: Decoded application message.
|
|
98
|
-
resume_token: Opaque token for the next read.
|
|
99
|
-
created_time: Server-assigned UTC creation time.
|
|
100
|
-
source: Informational client source or Step-generated ``#stepExecutionID``.
|
|
101
|
-
"""
|
|
102
|
-
|
|
103
|
-
value: ValueT
|
|
104
|
-
resume_token: str
|
|
105
|
-
created_time: datetime
|
|
106
|
-
source: str
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/sdk-rust/crates/dex-blob-cache-python/Cargo.toml
RENAMED
|
File without changes
|
|
File without changes
|
{dex_python_sdk-0.2.6 → dex_python_sdk-0.2.8}/sdk-rust/crates/dex-blob-cache-python/src/lib.rs
RENAMED
|
File without changes
|