dex-python-sdk 0.2.11__tar.gz → 0.3.2__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.
Files changed (62) hide show
  1. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/PKG-INFO +24 -1
  2. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/README.md +23 -0
  3. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/__init__.py +4 -1
  4. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/_async_value_hydrator.py +28 -2
  5. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/_async_worker_dispatcher.py +61 -8
  6. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/_invocation_context.py +35 -24
  7. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/_value_hydrator.py +28 -2
  8. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/_worker_dispatcher.py +205 -6
  9. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/async_client.py +26 -1
  10. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/client.py +26 -1
  11. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/context.py +23 -0
  12. dex_python_sdk-0.3.2/dex/dexpb/dex_pb2.py +495 -0
  13. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/dexpb/dex_pb2.pyi +124 -22
  14. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/flow.py +35 -4
  15. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/flow_options.py +69 -3
  16. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/step.py +26 -2
  17. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/pyproject.toml +1 -1
  18. dex_python_sdk-0.2.11/dex/dexpb/dex_pb2.py +0 -477
  19. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/LEGACY_NOTICES.md +0 -0
  20. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/LICENSE +0 -0
  21. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/_async_worker_service.py +0 -0
  22. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/_grpc_errors.py +0 -0
  23. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/_native.pyi +0 -0
  24. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/_utils.py +0 -0
  25. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/_value_mapper.py +0 -0
  26. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/_worker_service.py +0 -0
  27. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/async_worker.py +0 -0
  28. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/attribute.py +0 -0
  29. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/blob_cache.py +0 -0
  30. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/channel.py +0 -0
  31. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/client_options.py +0 -0
  32. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/codec.py +0 -0
  33. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/condition.py +0 -0
  34. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/dexpb/__init__.py +0 -0
  35. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/dexpb/dex_pb2_grpc.py +0 -0
  36. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/flow_config.py +0 -0
  37. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/flow_info.py +0 -0
  38. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/flow_result.py +0 -0
  39. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/py.typed +0 -0
  40. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/runtime_errors.py +0 -0
  41. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/step_execution.py +0 -0
  42. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/stream.py +0 -0
  43. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/subflow.py +0 -0
  44. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/timer.py +0 -0
  45. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/wait.py +0 -0
  46. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/worker.py +0 -0
  47. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/worker_options.py +0 -0
  48. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/sdk-rust/Cargo.lock +0 -0
  49. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/sdk-rust/Cargo.toml +0 -0
  50. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/sdk-rust/crates/dex-blob-cache/Cargo.toml +0 -0
  51. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/sdk-rust/crates/dex-blob-cache/LICENSE +0 -0
  52. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/sdk-rust/crates/dex-blob-cache/src/config.rs +0 -0
  53. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/sdk-rust/crates/dex-blob-cache/src/entry.rs +0 -0
  54. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/sdk-rust/crates/dex-blob-cache/src/error.rs +0 -0
  55. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/sdk-rust/crates/dex-blob-cache/src/format.rs +0 -0
  56. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/sdk-rust/crates/dex-blob-cache/src/lib.rs +0 -0
  57. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/sdk-rust/crates/dex-blob-cache/src/policy.rs +0 -0
  58. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/sdk-rust/crates/dex-blob-cache/src/store.rs +0 -0
  59. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/sdk-rust/crates/dex-blob-cache/tests/blob_cache_integration.rs +0 -0
  60. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/sdk-rust/crates/dex-blob-cache-python/Cargo.toml +0 -0
  61. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/sdk-rust/crates/dex-blob-cache-python/LICENSE +0 -0
  62. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/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.11
3
+ Version: 0.3.2
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
@@ -59,6 +59,29 @@ atomic commit and Channel deletion validation. Attribute locks add isolation
59
59
  only among cooperating Steps and RPCs using the same lock. Write-only publish
60
60
  and delete operations do not require loading.
61
61
 
62
+ ## Step and timeout-handler state loading
63
+
64
+ `StepOptions` provides the same five selections independently for `wait_for` and
65
+ `execute`: all AttributeMap instances, exact AttributeMap instances, Channels,
66
+ all ChannelMap instances, and exact ChannelMap instances. The methods receive
67
+ independent snapshots. Execute reads after the winning Wait consumes messages;
68
+ retries of one logical method call reuse its first snapshot.
69
+
70
+ `FlowTimeoutHandlerOptions` provides Execute-style timeouts, heartbeat detection,
71
+ retry, durability, Attribute locks, and the same state selections. Set it on
72
+ `StartFlowOptions` or `SubFlowOptions` only for a positive timeout using `HANDLER`.
73
+ Exhausted retries may proceed to a registered `Step[None]`; read the final
74
+ failure from `Context.recovery_error`.
75
+
76
+ ```python
77
+ class TimeoutRecoveryStep(Step[None]):
78
+ def execute(self, context: Context, input: None) -> StepDecision:
79
+ failure = context.recovery_error
80
+ if failure is None:
81
+ return force_fail("timeout recovery has no failure")
82
+ return force_fail(failure.detail)
83
+ ```
84
+
62
85
  Python SDK for [Dex workflow engine](https://github.com/superdurable/dex)
63
86
 
64
87
  ## New user contracts
@@ -43,6 +43,29 @@ atomic commit and Channel deletion validation. Attribute locks add isolation
43
43
  only among cooperating Steps and RPCs using the same lock. Write-only publish
44
44
  and delete operations do not require loading.
45
45
 
46
+ ## Step and timeout-handler state loading
47
+
48
+ `StepOptions` provides the same five selections independently for `wait_for` and
49
+ `execute`: all AttributeMap instances, exact AttributeMap instances, Channels,
50
+ all ChannelMap instances, and exact ChannelMap instances. The methods receive
51
+ independent snapshots. Execute reads after the winning Wait consumes messages;
52
+ retries of one logical method call reuse its first snapshot.
53
+
54
+ `FlowTimeoutHandlerOptions` provides Execute-style timeouts, heartbeat detection,
55
+ retry, durability, Attribute locks, and the same state selections. Set it on
56
+ `StartFlowOptions` or `SubFlowOptions` only for a positive timeout using `HANDLER`.
57
+ Exhausted retries may proceed to a registered `Step[None]`; read the final
58
+ failure from `Context.recovery_error`.
59
+
60
+ ```python
61
+ class TimeoutRecoveryStep(Step[None]):
62
+ def execute(self, context: Context, input: None) -> StepDecision:
63
+ failure = context.recovery_error
64
+ if failure is None:
65
+ return force_fail("timeout recovery has no failure")
66
+ return force_fail(failure.detail)
67
+ ```
68
+
46
69
  Python SDK for [Dex workflow engine](https://github.com/superdurable/dex)
47
70
 
48
71
  ## New user contracts
@@ -36,7 +36,7 @@ from dex.codec import (
36
36
  WireKind,
37
37
  )
38
38
  from dex.condition import ConditionCombination
39
- from dex.context import AsyncContext, Context
39
+ from dex.context import AsyncContext, Context, RecoveryErrorInfo
40
40
  from dex.flow import Flow, PersistenceSchema, Registry, RPCResult, rpc
41
41
  from dex.flow_config import ActiveStepSearchMode, FlowConfig
42
42
  from dex.flow_info import (
@@ -47,6 +47,7 @@ from dex.flow_info import (
47
47
  SearchFlowsPage,
48
48
  )
49
49
  from dex.flow_options import (
50
+ FlowTimeoutHandlerOptions,
50
51
  FlowTimeoutPolicy,
51
52
  IdReusePolicy,
52
53
  StartFlowOptions,
@@ -152,6 +153,7 @@ __all__ = [
152
153
  "FlowNotActiveError",
153
154
  "FlowNotFoundError",
154
155
  "FlowStatus",
156
+ "FlowTimeoutHandlerOptions",
155
157
  "FlowTimeoutPolicy",
156
158
  "HealthInfo",
157
159
  "IdReusePolicy",
@@ -161,6 +163,7 @@ __all__ = [
161
163
  "LongPollTimeoutError",
162
164
  "PersistenceSchema",
163
165
  "RPCResult",
166
+ "RecoveryErrorInfo",
164
167
  "RetryAfterError",
165
168
  "RpcLockConflictError",
166
169
  "Registry",
@@ -80,12 +80,25 @@ class AsyncValueHydrator:
80
80
  if has_heartbeat:
81
81
  values.append(request.context.last_heartbeat_value)
82
82
  values.extend(entry.value for entry in request.attributes)
83
+ channel_messages = [
84
+ message
85
+ for channel_values in request.loaded_channel_messages.values()
86
+ for message in channel_values.messages
87
+ ]
88
+ values.extend(message.value for message in channel_messages)
83
89
  hydrated = iter(await self.hydrate_all(values))
84
90
  result.step_input.CopyFrom(next(hydrated))
85
91
  if has_heartbeat:
86
92
  result.context.last_heartbeat_value.CopyFrom(next(hydrated))
87
- for entry, value in zip(result.attributes, hydrated):
88
- entry.value.CopyFrom(value)
93
+ for entry in result.attributes:
94
+ entry.value.CopyFrom(next(hydrated))
95
+ result_messages = [
96
+ message
97
+ for channel_values in result.loaded_channel_messages.values()
98
+ for message in channel_values.messages
99
+ ]
100
+ for message in result_messages:
101
+ message.value.CopyFrom(next(hydrated))
89
102
  return result
90
103
 
91
104
  async def execute_request(
@@ -101,6 +114,12 @@ class AsyncValueHydrator:
101
114
  values.append(request.context.last_heartbeat_value)
102
115
  values.extend(entry.value for entry in request.attributes)
103
116
  values.extend(entry.value for entry in request.step_exe_locals)
117
+ channel_messages = [
118
+ message
119
+ for channel_values in request.loaded_channel_messages.values()
120
+ for message in channel_values.messages
121
+ ]
122
+ values.extend(message.value for message in channel_messages)
104
123
  for channel_result in request.condition_results.channel_results:
105
124
  values.extend(channel_result.values)
106
125
  hydrated = iter(await self.hydrate_all(values))
@@ -112,6 +131,13 @@ class AsyncValueHydrator:
112
131
  entry.value.CopyFrom(next(hydrated))
113
132
  for entry in result.step_exe_locals:
114
133
  entry.value.CopyFrom(next(hydrated))
134
+ result_messages = [
135
+ message
136
+ for channel_values in result.loaded_channel_messages.values()
137
+ for message in channel_values.messages
138
+ ]
139
+ for message in result_messages:
140
+ message.value.CopyFrom(next(hydrated))
115
141
  for channel_result in result.condition_results.channel_results:
116
142
  for value in channel_result.values:
117
143
  value.CopyFrom(next(hydrated))
@@ -100,6 +100,11 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
100
100
  request.context,
101
101
  self._values,
102
102
  request.attributes,
103
+ channel_infos=dict(request.channel_infos),
104
+ loaded_channel_messages=dict(request.loaded_channel_messages),
105
+ loaded_attribute_map_instances=request.loaded_attribute_map_instances,
106
+ loaded_channel_names=request.loaded_channel_names,
107
+ loaded_channel_map_instances=request.loaded_channel_map_instances,
103
108
  is_active=is_active,
104
109
  output_emitter=emitter,
105
110
  )
@@ -126,6 +131,7 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
126
131
  upsert_step_exe_locals=list(context.local_writes.values()),
127
132
  record_events=context.events,
128
133
  publish_to_channel=context.publications,
134
+ delete_from_channel=context.channel_deletions,
129
135
  )
130
136
  waiting = self._map_wait(flow, wait)
131
137
  if waiting is not None:
@@ -153,8 +159,16 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
153
159
  is_active: Callable[[], bool] | None = None,
154
160
  ) -> AsyncGenerator[pb.InvokeExecuteMethodOutput, None]:
155
161
  if original.step_type == _TIMEOUT_HANDLER_STEP_TYPE:
156
- response = await self._invoke_timeout_handler_async(original)
157
- yield pb.InvokeExecuteMethodOutput(result=response)
162
+ emitter = _AsyncStepOutputEmitter()
163
+ handler = asyncio.create_task(
164
+ self._invoke_timeout_handler_async(original, emitter, is_active)
165
+ )
166
+ try:
167
+ async for output in self._drain_outputs(emitter, handler):
168
+ yield self._map_execute_output(output)
169
+ yield pb.InvokeExecuteMethodOutput(result=await handler)
170
+ finally:
171
+ await self._close_invocation(emitter, handler)
158
172
  return
159
173
  emitter = _AsyncStepOutputEmitter()
160
174
  handler = asyncio.create_task(
@@ -187,6 +201,11 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
187
201
  request.attributes,
188
202
  request.step_exe_locals,
189
203
  condition_results,
204
+ channel_infos=dict(request.channel_infos),
205
+ loaded_channel_messages=dict(request.loaded_channel_messages),
206
+ loaded_attribute_map_instances=request.loaded_attribute_map_instances,
207
+ loaded_channel_names=request.loaded_channel_names,
208
+ loaded_channel_map_instances=request.loaded_channel_map_instances,
190
209
  is_active=is_active,
191
210
  output_emitter=emitter,
192
211
  )
@@ -214,6 +233,7 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
214
233
  record_events=context.events,
215
234
  upsert_step_exe_locals=list(context.local_writes.values()),
216
235
  publish_to_channel=context.publications,
236
+ delete_from_channel=context.channel_deletions,
217
237
  )
218
238
  except InvalidStepResultError as error:
219
239
  failure = error
@@ -235,6 +255,8 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
235
255
  async def _invoke_timeout_handler_async(
236
256
  self,
237
257
  original: pb.InvokeExecuteMethodRequest,
258
+ emitter: _AsyncStepOutputEmitter,
259
+ is_active: Callable[[], bool] | None,
238
260
  ) -> pb.InvokeExecuteMethodResponse:
239
261
  request = await self._async_hydrator.execute_request(original)
240
262
  flow = self._registry._flow_by_type(request.flow_type)
@@ -260,24 +282,55 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
260
282
  request.attributes,
261
283
  request.step_exe_locals,
262
284
  condition_results,
285
+ channel_infos=dict(request.channel_infos),
286
+ loaded_channel_messages=dict(request.loaded_channel_messages),
287
+ loaded_attribute_map_instances=request.loaded_attribute_map_instances,
288
+ loaded_channel_names=request.loaded_channel_names,
289
+ loaded_channel_map_instances=request.loaded_channel_map_instances,
290
+ is_active=is_active,
291
+ output_emitter=emitter,
263
292
  )
264
- decision: Any = flow.flow.handle_timeout(context)
265
- if isawaitable(decision):
266
- decision = await decision
293
+ response: pb.InvokeExecuteMethodResponse | None = None
294
+ failure: BaseException | None = None
295
+ cause: BaseException | None = None
267
296
  try:
297
+ decision: Any = flow.flow.handle_timeout(context)
298
+ if isgenerator(decision):
299
+ decision.close()
300
+ raise InvalidStepResultError(
301
+ flow.name,
302
+ _TIMEOUT_HANDLER_STEP_TYPE,
303
+ "execute",
304
+ "synchronous generators require Worker",
305
+ )
306
+ if isawaitable(decision):
307
+ decision = await decision
268
308
  if not isinstance(decision, StepDecision):
269
309
  raise TypeError("handle_timeout must return StepDecision")
270
- return pb.InvokeExecuteMethodResponse(
310
+ response = pb.InvokeExecuteMethodResponse(
271
311
  step_decision=self._map_decision(flow, decision),
272
312
  upsert_attributes=list(context.attribute_writes.values()),
273
313
  record_events=context.events,
274
314
  upsert_step_exe_locals=list(context.local_writes.values()),
275
315
  publish_to_channel=context.publications,
316
+ delete_from_channel=context.channel_deletions,
276
317
  )
318
+ except InvalidStepResultError as error:
319
+ failure = error
277
320
  except (TypeError, ValueError) as error:
278
- raise InvalidStepResultError(
321
+ failure = InvalidStepResultError(
279
322
  flow.name, _TIMEOUT_HANDLER_STEP_TYPE, "execute", str(error)
280
- ) from error
323
+ )
324
+ cause = error
325
+ except BaseException as error:
326
+ failure = error
327
+ combined = context._finalize_step_outputs(failure)
328
+ if combined is not None:
329
+ if cause is not None:
330
+ raise combined from cause
331
+ raise combined
332
+ assert response is not None
333
+ return response
281
334
 
282
335
  async def invoke_rpc( # type: ignore[override]
283
336
  self,
@@ -17,6 +17,7 @@ from dex._value_mapper import ValueMapper
17
17
  from dex.attribute import Attribute, AttributeMap, _apply_attribute_store_sync
18
18
  from dex.channel import Channel, ChannelMap, ChannelMessage
19
19
  from dex.codec import Codec
20
+ from dex.context import RecoveryErrorInfo
20
21
  from dex.dexpb import dex_pb2 as pb
21
22
  from dex.flow import Registry, _RegisteredFlow
22
23
  from dex.flow_result import FlowResult, flow_result_from_proto
@@ -116,6 +117,15 @@ class InvocationContext:
116
117
  def from_step_execution_id(self) -> str:
117
118
  return self._metadata.from_step_execution_id
118
119
 
120
+ @property
121
+ def recovery_error(self) -> RecoveryErrorInfo | None:
122
+ if not self._metadata.HasField("recovery_error"):
123
+ return None
124
+ return RecoveryErrorInfo(
125
+ detail=self._metadata.recovery_error.detail,
126
+ error_type=self._metadata.recovery_error.error_type,
127
+ )
128
+
119
129
  @property
120
130
  def attempt(self) -> int:
121
131
  return self._metadata.attempt
@@ -139,7 +149,12 @@ class InvocationContext:
139
149
 
140
150
  async def heartbeat(self, value: object = _NO_HEARTBEAT_VALUE) -> None:
141
151
  if (
142
- self._method not in (InvocationMethod.WAIT_FOR, InvocationMethod.EXECUTE)
152
+ self._method
153
+ not in (
154
+ InvocationMethod.WAIT_FOR,
155
+ InvocationMethod.EXECUTE,
156
+ InvocationMethod.TIMEOUT,
157
+ )
143
158
  or self._output_emitter is None
144
159
  ):
145
160
  raise ValueError("heartbeat requires an asynchronous Step Context")
@@ -226,7 +241,11 @@ class InvocationContext:
226
241
  definition: Stream[ValueT],
227
242
  value: ValueT,
228
243
  ) -> StepOutput | None:
229
- if self._method not in (InvocationMethod.WAIT_FOR, InvocationMethod.EXECUTE):
244
+ if self._method not in (
245
+ InvocationMethod.WAIT_FOR,
246
+ InvocationMethod.EXECUTE,
247
+ InvocationMethod.TIMEOUT,
248
+ ):
230
249
  raise ValueError("Stream writes require a Step Context")
231
250
  self._require_registered(definition)
232
251
  output = _StreamStepOutput(
@@ -245,7 +264,11 @@ class InvocationContext:
245
264
  return None
246
265
 
247
266
  def _prepare_buffered_stream(self, definition: Stream[object]) -> bool:
248
- if self._method not in (InvocationMethod.WAIT_FOR, InvocationMethod.EXECUTE):
267
+ if self._method not in (
268
+ InvocationMethod.WAIT_FOR,
269
+ InvocationMethod.EXECUTE,
270
+ InvocationMethod.TIMEOUT,
271
+ ):
249
272
  raise ValueError("Buffered Streams require a Step Context")
250
273
  self._require_registered(definition)
251
274
  return self._output_emitter is not None
@@ -357,11 +380,10 @@ class InvocationContext:
357
380
  ),
358
381
  )
359
382
  )
360
- if self._method is InvocationMethod.RPC:
361
- current = self._channel_infos.get(name)
362
- self._channel_infos[name] = pb.ChannelInfo(
363
- size=(current.size if current is not None else 0) + 1
364
- )
383
+ current = self._channel_infos.get(name)
384
+ self._channel_infos[name] = pb.ChannelInfo(
385
+ size=(current.size if current is not None else 0) + 1
386
+ )
365
387
 
366
388
  def _delete_channel_message(
367
389
  self,
@@ -369,8 +391,6 @@ class InvocationContext:
369
391
  instance: str | None,
370
392
  message_id: str,
371
393
  ) -> None:
372
- if self._method is not InvocationMethod.RPC:
373
- raise ValueError("Channel message deletion requires an RPC Context")
374
394
  self._require_registered(definition)
375
395
  name = self._physical_name(definition, instance)
376
396
  self.channel_deletions.append(
@@ -410,8 +430,6 @@ class InvocationContext:
410
430
  definition: ChannelMap[object],
411
431
  ) -> tuple[str, ...]:
412
432
  self._require_registered(definition)
413
- if self._method is not InvocationMethod.RPC:
414
- raise ValueError("ChannelMap introspection requires an RPC invocation")
415
433
  prefix = f"{definition.name}/"
416
434
  return tuple(
417
435
  sorted(
@@ -435,8 +453,6 @@ class InvocationContext:
435
453
  definition: Channel[ValueT] | ChannelMap[ValueT],
436
454
  instance: str | None,
437
455
  ) -> tuple[ChannelMessage[ValueT], ...]:
438
- if self._method is not InvocationMethod.RPC:
439
- raise ValueError("pending Channel messages require an RPC Context")
440
456
  self._require_registered(definition)
441
457
  if isinstance(definition, ChannelMap):
442
458
  channel_name = self._physical_name(definition, instance)
@@ -449,7 +465,7 @@ class InvocationContext:
449
465
  is_loaded = definition.name in self._loaded_channel_names
450
466
  if not is_loaded:
451
467
  raise ChannelMessagesNotLoadedError(
452
- f"Channel messages were not loaded for RPC: {definition.name}"
468
+ f"Channel messages were not loaded for this invocation: {definition.name}"
453
469
  )
454
470
  values = self._loaded_channel_messages.get(channel_name)
455
471
  if values is None:
@@ -489,9 +505,7 @@ class InvocationContext:
489
505
  definition: Attribute[Any] | AttributeMap[Any],
490
506
  instance: str | None,
491
507
  ) -> None:
492
- if self._method is not InvocationMethod.RPC or not isinstance(
493
- definition, AttributeMap
494
- ):
508
+ if not isinstance(definition, AttributeMap):
495
509
  return
496
510
  physical_name = self._physical_name(definition, instance)
497
511
  if (
@@ -499,19 +513,16 @@ class InvocationContext:
499
513
  and physical_name not in self._loaded_attribute_map_instances
500
514
  ):
501
515
  raise AttributeMapNotLoadedError(
502
- f"AttributeMap instance was not loaded for RPC: {physical_name}"
516
+ f"AttributeMap instance was not loaded for this invocation: {physical_name}"
503
517
  )
504
518
 
505
519
  def _require_attribute_map_all_loaded(
506
520
  self,
507
521
  definition: AttributeMap[object],
508
522
  ) -> None:
509
- if (
510
- self._method is InvocationMethod.RPC
511
- and f"{definition.name}/" not in self._loaded_attribute_map_instances
512
- ):
523
+ if f"{definition.name}/" not in self._loaded_attribute_map_instances:
513
524
  raise AttributeMapNotLoadedError(
514
- f"all AttributeMap instances were not loaded for RPC: {definition.name}"
525
+ f"all AttributeMap instances were not loaded for this invocation: {definition.name}"
515
526
  )
516
527
 
517
528
  def _require_registered(self, definition: _Definition) -> None:
@@ -80,12 +80,25 @@ class ValueHydrator:
80
80
  if has_heartbeat:
81
81
  values.append(request.context.last_heartbeat_value)
82
82
  values.extend(entry.value for entry in request.attributes)
83
+ channel_messages = [
84
+ message
85
+ for channel_values in request.loaded_channel_messages.values()
86
+ for message in channel_values.messages
87
+ ]
88
+ values.extend(message.value for message in channel_messages)
83
89
  hydrated = iter(self.hydrate_all(values))
84
90
  result.step_input.CopyFrom(next(hydrated))
85
91
  if has_heartbeat:
86
92
  result.context.last_heartbeat_value.CopyFrom(next(hydrated))
87
- for entry, value in zip(result.attributes, hydrated):
88
- entry.value.CopyFrom(value)
93
+ for entry in result.attributes:
94
+ entry.value.CopyFrom(next(hydrated))
95
+ result_messages = [
96
+ message
97
+ for channel_values in result.loaded_channel_messages.values()
98
+ for message in channel_values.messages
99
+ ]
100
+ for message in result_messages:
101
+ message.value.CopyFrom(next(hydrated))
89
102
  return result
90
103
 
91
104
  def execute_request(
@@ -101,6 +114,12 @@ class ValueHydrator:
101
114
  values.append(request.context.last_heartbeat_value)
102
115
  values.extend(entry.value for entry in request.attributes)
103
116
  values.extend(entry.value for entry in request.step_exe_locals)
117
+ channel_messages = [
118
+ message
119
+ for channel_values in request.loaded_channel_messages.values()
120
+ for message in channel_values.messages
121
+ ]
122
+ values.extend(message.value for message in channel_messages)
104
123
  for channel_result in request.condition_results.channel_results:
105
124
  values.extend(channel_result.values)
106
125
  for flow_result in request.condition_results.sub_flow_results:
@@ -116,6 +135,13 @@ class ValueHydrator:
116
135
  entry.value.CopyFrom(next(hydrated))
117
136
  for entry in result.step_exe_locals:
118
137
  entry.value.CopyFrom(next(hydrated))
138
+ result_messages = [
139
+ message
140
+ for channel_values in result.loaded_channel_messages.values()
141
+ for message in channel_values.messages
142
+ ]
143
+ for message in result_messages:
144
+ message.value.CopyFrom(next(hydrated))
119
145
  for channel_result in result.condition_results.channel_results:
120
146
  for value in channel_result.values:
121
147
  value.CopyFrom(next(hydrated))