dex-python-sdk 0.2.11__tar.gz → 0.3.1__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.1}/PKG-INFO +1 -1
  2. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/__init__.py +2 -0
  3. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/_async_value_hydrator.py +28 -2
  4. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/_async_worker_dispatcher.py +61 -8
  5. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/_invocation_context.py +25 -24
  6. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/_value_hydrator.py +28 -2
  7. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/_worker_dispatcher.py +205 -6
  8. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/async_client.py +26 -1
  9. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/client.py +26 -1
  10. dex_python_sdk-0.3.1/dex/dexpb/dex_pb2.py +495 -0
  11. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/dexpb/dex_pb2.pyi +124 -22
  12. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/flow.py +35 -4
  13. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/flow_options.py +69 -3
  14. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/step.py +26 -2
  15. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/pyproject.toml +1 -1
  16. dex_python_sdk-0.2.11/dex/dexpb/dex_pb2.py +0 -477
  17. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/LEGACY_NOTICES.md +0 -0
  18. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/LICENSE +0 -0
  19. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/README.md +0 -0
  20. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/_async_worker_service.py +0 -0
  21. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/_grpc_errors.py +0 -0
  22. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/_native.pyi +0 -0
  23. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/_utils.py +0 -0
  24. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/_value_mapper.py +0 -0
  25. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/_worker_service.py +0 -0
  26. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/async_worker.py +0 -0
  27. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/attribute.py +0 -0
  28. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/blob_cache.py +0 -0
  29. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/channel.py +0 -0
  30. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/client_options.py +0 -0
  31. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/codec.py +0 -0
  32. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/condition.py +0 -0
  33. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/context.py +0 -0
  34. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/dexpb/__init__.py +0 -0
  35. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/dexpb/dex_pb2_grpc.py +0 -0
  36. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/flow_config.py +0 -0
  37. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/flow_info.py +0 -0
  38. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/flow_result.py +0 -0
  39. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/py.typed +0 -0
  40. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/runtime_errors.py +0 -0
  41. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/step_execution.py +0 -0
  42. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/stream.py +0 -0
  43. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/subflow.py +0 -0
  44. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/timer.py +0 -0
  45. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/wait.py +0 -0
  46. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/worker.py +0 -0
  47. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/worker_options.py +0 -0
  48. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/sdk-rust/Cargo.lock +0 -0
  49. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/sdk-rust/Cargo.toml +0 -0
  50. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/sdk-rust/crates/dex-blob-cache/Cargo.toml +0 -0
  51. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/sdk-rust/crates/dex-blob-cache/LICENSE +0 -0
  52. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/sdk-rust/crates/dex-blob-cache/src/config.rs +0 -0
  53. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/sdk-rust/crates/dex-blob-cache/src/entry.rs +0 -0
  54. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/sdk-rust/crates/dex-blob-cache/src/error.rs +0 -0
  55. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/sdk-rust/crates/dex-blob-cache/src/format.rs +0 -0
  56. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/sdk-rust/crates/dex-blob-cache/src/lib.rs +0 -0
  57. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/sdk-rust/crates/dex-blob-cache/src/policy.rs +0 -0
  58. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/sdk-rust/crates/dex-blob-cache/src/store.rs +0 -0
  59. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/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.1}/sdk-rust/crates/dex-blob-cache-python/Cargo.toml +0 -0
  61. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/sdk-rust/crates/dex-blob-cache-python/LICENSE +0 -0
  62. {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/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.1
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
@@ -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",
@@ -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,
@@ -139,7 +139,12 @@ class InvocationContext:
139
139
 
140
140
  async def heartbeat(self, value: object = _NO_HEARTBEAT_VALUE) -> None:
141
141
  if (
142
- self._method not in (InvocationMethod.WAIT_FOR, InvocationMethod.EXECUTE)
142
+ self._method
143
+ not in (
144
+ InvocationMethod.WAIT_FOR,
145
+ InvocationMethod.EXECUTE,
146
+ InvocationMethod.TIMEOUT,
147
+ )
143
148
  or self._output_emitter is None
144
149
  ):
145
150
  raise ValueError("heartbeat requires an asynchronous Step Context")
@@ -226,7 +231,11 @@ class InvocationContext:
226
231
  definition: Stream[ValueT],
227
232
  value: ValueT,
228
233
  ) -> StepOutput | None:
229
- if self._method not in (InvocationMethod.WAIT_FOR, InvocationMethod.EXECUTE):
234
+ if self._method not in (
235
+ InvocationMethod.WAIT_FOR,
236
+ InvocationMethod.EXECUTE,
237
+ InvocationMethod.TIMEOUT,
238
+ ):
230
239
  raise ValueError("Stream writes require a Step Context")
231
240
  self._require_registered(definition)
232
241
  output = _StreamStepOutput(
@@ -245,7 +254,11 @@ class InvocationContext:
245
254
  return None
246
255
 
247
256
  def _prepare_buffered_stream(self, definition: Stream[object]) -> bool:
248
- if self._method not in (InvocationMethod.WAIT_FOR, InvocationMethod.EXECUTE):
257
+ if self._method not in (
258
+ InvocationMethod.WAIT_FOR,
259
+ InvocationMethod.EXECUTE,
260
+ InvocationMethod.TIMEOUT,
261
+ ):
249
262
  raise ValueError("Buffered Streams require a Step Context")
250
263
  self._require_registered(definition)
251
264
  return self._output_emitter is not None
@@ -357,11 +370,10 @@ class InvocationContext:
357
370
  ),
358
371
  )
359
372
  )
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
- )
373
+ current = self._channel_infos.get(name)
374
+ self._channel_infos[name] = pb.ChannelInfo(
375
+ size=(current.size if current is not None else 0) + 1
376
+ )
365
377
 
366
378
  def _delete_channel_message(
367
379
  self,
@@ -369,8 +381,6 @@ class InvocationContext:
369
381
  instance: str | None,
370
382
  message_id: str,
371
383
  ) -> None:
372
- if self._method is not InvocationMethod.RPC:
373
- raise ValueError("Channel message deletion requires an RPC Context")
374
384
  self._require_registered(definition)
375
385
  name = self._physical_name(definition, instance)
376
386
  self.channel_deletions.append(
@@ -410,8 +420,6 @@ class InvocationContext:
410
420
  definition: ChannelMap[object],
411
421
  ) -> tuple[str, ...]:
412
422
  self._require_registered(definition)
413
- if self._method is not InvocationMethod.RPC:
414
- raise ValueError("ChannelMap introspection requires an RPC invocation")
415
423
  prefix = f"{definition.name}/"
416
424
  return tuple(
417
425
  sorted(
@@ -435,8 +443,6 @@ class InvocationContext:
435
443
  definition: Channel[ValueT] | ChannelMap[ValueT],
436
444
  instance: str | None,
437
445
  ) -> tuple[ChannelMessage[ValueT], ...]:
438
- if self._method is not InvocationMethod.RPC:
439
- raise ValueError("pending Channel messages require an RPC Context")
440
446
  self._require_registered(definition)
441
447
  if isinstance(definition, ChannelMap):
442
448
  channel_name = self._physical_name(definition, instance)
@@ -449,7 +455,7 @@ class InvocationContext:
449
455
  is_loaded = definition.name in self._loaded_channel_names
450
456
  if not is_loaded:
451
457
  raise ChannelMessagesNotLoadedError(
452
- f"Channel messages were not loaded for RPC: {definition.name}"
458
+ f"Channel messages were not loaded for this invocation: {definition.name}"
453
459
  )
454
460
  values = self._loaded_channel_messages.get(channel_name)
455
461
  if values is None:
@@ -489,9 +495,7 @@ class InvocationContext:
489
495
  definition: Attribute[Any] | AttributeMap[Any],
490
496
  instance: str | None,
491
497
  ) -> None:
492
- if self._method is not InvocationMethod.RPC or not isinstance(
493
- definition, AttributeMap
494
- ):
498
+ if not isinstance(definition, AttributeMap):
495
499
  return
496
500
  physical_name = self._physical_name(definition, instance)
497
501
  if (
@@ -499,19 +503,16 @@ class InvocationContext:
499
503
  and physical_name not in self._loaded_attribute_map_instances
500
504
  ):
501
505
  raise AttributeMapNotLoadedError(
502
- f"AttributeMap instance was not loaded for RPC: {physical_name}"
506
+ f"AttributeMap instance was not loaded for this invocation: {physical_name}"
503
507
  )
504
508
 
505
509
  def _require_attribute_map_all_loaded(
506
510
  self,
507
511
  definition: AttributeMap[object],
508
512
  ) -> None:
509
- if (
510
- self._method is InvocationMethod.RPC
511
- and f"{definition.name}/" not in self._loaded_attribute_map_instances
512
- ):
513
+ if f"{definition.name}/" not in self._loaded_attribute_map_instances:
513
514
  raise AttributeMapNotLoadedError(
514
- f"all AttributeMap instances were not loaded for RPC: {definition.name}"
515
+ f"all AttributeMap instances were not loaded for this invocation: {definition.name}"
515
516
  )
516
517
 
517
518
  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))
@@ -15,13 +15,19 @@ from typing import Any, Callable, Generator, cast
15
15
  from dex._invocation_context import InvocationContext, InvocationMethod
16
16
  from dex._value_hydrator import ValueHydrator
17
17
  from dex._value_mapper import ValueMapper
18
- from dex.attribute import AttributeLock, AttributeMap, _apply_attribute_store_sync
19
- from dex.channel import Channel, ChannelMap
18
+ from dex.attribute import (
19
+ AttributeLock,
20
+ AttributeMap,
21
+ AttributeMapLoad,
22
+ _apply_attribute_store_sync,
23
+ )
24
+ from dex.channel import Channel, ChannelMap, ChannelMapLoad
20
25
  from dex.condition import ChannelCondition, Condition, SubFlowCondition, TimerCondition
21
26
  from dex.dexpb import dex_pb2 as pb
22
27
  from dex.flow import Registry, RPCResult, _RegisteredFlow, _RegisteredStep
23
28
  from dex.flow_config import ActiveStepSearchMode, FlowConfig
24
29
  from dex.flow_options import (
30
+ FlowTimeoutHandlerOptions,
25
31
  FlowTimeoutPolicy,
26
32
  SubFlowOptions,
27
33
  SubFlowReusePolicy,
@@ -71,6 +77,11 @@ class WorkerDispatcher:
71
77
  request.context,
72
78
  self._values,
73
79
  request.attributes,
80
+ channel_infos=dict(request.channel_infos),
81
+ loaded_channel_messages=dict(request.loaded_channel_messages),
82
+ loaded_attribute_map_instances=request.loaded_attribute_map_instances,
83
+ loaded_channel_names=request.loaded_channel_names,
84
+ loaded_channel_map_instances=request.loaded_channel_map_instances,
74
85
  is_active=is_active,
75
86
  )
76
87
  input = self._values.decode(request.step_input, step.input_codec)
@@ -92,6 +103,7 @@ class WorkerDispatcher:
92
103
  upsert_step_exe_locals=list(context.local_writes.values()),
93
104
  record_events=context.events,
94
105
  publish_to_channel=context.publications,
106
+ delete_from_channel=context.channel_deletions,
95
107
  )
96
108
  waiting = self._map_wait(flow, wait)
97
109
  if waiting is not None:
@@ -110,9 +122,8 @@ class WorkerDispatcher:
110
122
  request = self._hydrator.execute_request(original)
111
123
  flow = self._registry._flow_by_type(request.flow_type)
112
124
  if request.step_type == _TIMEOUT_HANDLER_STEP_TYPE:
113
- yield pb.InvokeExecuteMethodOutput(
114
- result=self._invoke_timeout_handler(request, flow)
115
- )
125
+ response = yield from self._invoke_timeout_handler(request, flow)
126
+ yield pb.InvokeExecuteMethodOutput(result=response)
116
127
  return
117
128
  step = flow.step(request.step_type)
118
129
  condition_results = (
@@ -126,6 +137,11 @@ class WorkerDispatcher:
126
137
  request.attributes,
127
138
  request.step_exe_locals,
128
139
  condition_results,
140
+ channel_infos=dict(request.channel_infos),
141
+ loaded_channel_messages=dict(request.loaded_channel_messages),
142
+ loaded_attribute_map_instances=request.loaded_attribute_map_instances,
143
+ loaded_channel_names=request.loaded_channel_names,
144
+ loaded_channel_map_instances=request.loaded_channel_map_instances,
129
145
  is_active=is_active,
130
146
  )
131
147
  input = self._values.decode(request.step_input, step.input_codec)
@@ -149,6 +165,7 @@ class WorkerDispatcher:
149
165
  record_events=context.events,
150
166
  upsert_step_exe_locals=list(context.local_writes.values()),
151
167
  publish_to_channel=context.publications,
168
+ delete_from_channel=context.channel_deletions,
152
169
  )
153
170
  )
154
171
  except (TypeError, ValueError) as error:
@@ -160,7 +177,7 @@ class WorkerDispatcher:
160
177
  self,
161
178
  request: pb.InvokeExecuteMethodRequest,
162
179
  flow: _RegisteredFlow,
163
- ) -> pb.InvokeExecuteMethodResponse:
180
+ ) -> Generator[pb.InvokeExecuteMethodOutput, None, pb.InvokeExecuteMethodResponse]:
164
181
  if request.HasField("step_input"):
165
182
  raise InvalidStepResultError(
166
183
  flow.name, _TIMEOUT_HANDLER_STEP_TYPE, "execute", "input must be absent"
@@ -183,8 +200,29 @@ class WorkerDispatcher:
183
200
  request.attributes,
184
201
  request.step_exe_locals,
185
202
  condition_results,
203
+ channel_infos=dict(request.channel_infos),
204
+ loaded_channel_messages=dict(request.loaded_channel_messages),
205
+ loaded_attribute_map_instances=request.loaded_attribute_map_instances,
206
+ loaded_channel_names=request.loaded_channel_names,
207
+ loaded_channel_map_instances=request.loaded_channel_map_instances,
186
208
  )
187
209
  decision = flow.flow.handle_timeout(context)
210
+ if isgenerator(decision):
211
+ while True:
212
+ try:
213
+ output = next(decision)
214
+ except StopIteration as completion:
215
+ decision = completion.value
216
+ break
217
+ try:
218
+ yield self._map_execute_output(output)
219
+ except (TypeError, ValueError) as error:
220
+ raise InvalidStepResultError(
221
+ flow.name,
222
+ _TIMEOUT_HANDLER_STEP_TYPE,
223
+ "execute",
224
+ str(error),
225
+ ) from error
188
226
  try:
189
227
  if isawaitable(decision):
190
228
  raise TypeError(
@@ -198,6 +236,7 @@ class WorkerDispatcher:
198
236
  record_events=context.events,
199
237
  upsert_step_exe_locals=list(context.local_writes.values()),
200
238
  publish_to_channel=context.publications,
239
+ delete_from_channel=context.channel_deletions,
201
240
  )
202
241
  except (TypeError, ValueError) as error:
203
242
  raise InvalidStepResultError(
@@ -402,8 +441,160 @@ class WorkerDispatcher:
402
441
  mapped.execute_failure_proceed_step_options.skip_wait_for = (
403
442
  target.skips_wait_for
404
443
  )
444
+ wait_for_loads = self._map_handler_state_loads(
445
+ flow,
446
+ "WaitFor",
447
+ options.wait_for_load_attribute_maps,
448
+ options.wait_for_load_attribute_map_instances,
449
+ options.wait_for_load_channels,
450
+ options.wait_for_load_channel_maps,
451
+ options.wait_for_load_channel_map_instances,
452
+ )
453
+ mapped.wait_for_load_attribute_map_instances.extend(wait_for_loads[0])
454
+ mapped.wait_for_load_channel_names.extend(wait_for_loads[1])
455
+ mapped.wait_for_load_channel_map_instances.extend(wait_for_loads[2])
456
+ execute_loads = self._map_handler_state_loads(
457
+ flow,
458
+ "Execute",
459
+ options.execute_load_attribute_maps,
460
+ options.execute_load_attribute_map_instances,
461
+ options.execute_load_channels,
462
+ options.execute_load_channel_maps,
463
+ options.execute_load_channel_map_instances,
464
+ )
465
+ mapped.execute_load_attribute_map_instances.extend(execute_loads[0])
466
+ mapped.execute_load_channel_names.extend(execute_loads[1])
467
+ mapped.execute_load_channel_map_instances.extend(execute_loads[2])
405
468
  return mapped
406
469
 
470
+ def map_flow_timeout_handler_options(
471
+ self,
472
+ flow: _RegisteredFlow,
473
+ timeout: timedelta | None,
474
+ policy: FlowTimeoutPolicy,
475
+ options: FlowTimeoutHandlerOptions | None,
476
+ ) -> pb.FlowTimeoutHandlerOptions | None:
477
+ """Map validated timeout-handler options for a Flow start."""
478
+ if options is None:
479
+ return None
480
+ if (
481
+ timeout is None
482
+ or timeout.total_seconds() <= 0
483
+ or policy is not FlowTimeoutPolicy.HANDLER
484
+ ):
485
+ raise ValueError(
486
+ "timeout handler options require a positive timeout with HANDLER policy"
487
+ )
488
+ mapped = pb.FlowTimeoutHandlerOptions(
489
+ durability_override=cast(Any, self._map_durability(options.durability)),
490
+ lock_attribute_keys=[
491
+ self._map_lock(lock) for lock in options.lock_attributes
492
+ ],
493
+ )
494
+ if options.method_timeout is not None:
495
+ mapped.method_timeout_seconds = self._seconds32(options.method_timeout)
496
+ if options.heartbeat_timeout is not None:
497
+ mapped.heartbeat_timeout_seconds = self._seconds32(
498
+ options.heartbeat_timeout
499
+ )
500
+ if options.retry is not None:
501
+ mapped.retry_policy.CopyFrom(self._map_retry(options.retry))
502
+ state_loads = self._map_handler_state_loads(
503
+ flow,
504
+ "timeout handler",
505
+ options.load_attribute_maps,
506
+ options.load_attribute_map_instances,
507
+ options.load_channels,
508
+ options.load_channel_maps,
509
+ options.load_channel_map_instances,
510
+ )
511
+ mapped.load_attribute_map_instances.extend(state_loads[0])
512
+ mapped.load_channel_names.extend(state_loads[1])
513
+ mapped.load_channel_map_instances.extend(state_loads[2])
514
+ if options._failure_target is not None:
515
+ target = self._registered_movement_target(flow, options._failure_target)
516
+ if target.input_type not in (None, type(None)):
517
+ raise ValueError("timeout handler failure Step must use None input")
518
+ mapped.failure_policy = (
519
+ pb.EXECUTE_METHOD_FAILURE_POLICY_PROCEED_TO_CONFIGURED_STEP
520
+ )
521
+ mapped.failure_proceed_step_type = target.name
522
+ target_options = self.map_step_options(
523
+ flow,
524
+ (
525
+ options._failure_options
526
+ if options._failure_options is not None
527
+ else target.step.get_step_options()
528
+ ),
529
+ )
530
+ if target_options is not None:
531
+ mapped.failure_proceed_step_options.CopyFrom(target_options)
532
+ mapped.failure_proceed_step_options.skip_wait_for = target.skips_wait_for
533
+ return mapped
534
+
535
+ @staticmethod
536
+ def _map_handler_state_loads(
537
+ flow: _RegisteredFlow,
538
+ source: str,
539
+ attribute_maps: tuple[AttributeMap[Any], ...],
540
+ attribute_map_instances: tuple[AttributeMapLoad, ...],
541
+ channels: tuple[Channel[Any], ...],
542
+ channel_maps: tuple[ChannelMap[Any], ...],
543
+ channel_map_instances: tuple[ChannelMapLoad, ...],
544
+ ) -> tuple[tuple[str, ...], tuple[str, ...], tuple[str, ...]]:
545
+ mapped_attribute_maps: list[str] = []
546
+ for attribute_map in attribute_maps:
547
+ WorkerDispatcher._require_state_load(
548
+ flow, source, attribute_map, AttributeMap
549
+ )
550
+ mapped_attribute_maps.append(f"{attribute_map.name}/")
551
+ for attribute_load in attribute_map_instances:
552
+ if not isinstance(attribute_load, AttributeMapLoad):
553
+ raise TypeError(f"{source} has an invalid AttributeMap instance load")
554
+ WorkerDispatcher._require_state_load(
555
+ flow, source, attribute_load.attribute_map, AttributeMap
556
+ )
557
+ mapped_attribute_maps.append(attribute_load.physical_name)
558
+ mapped_channels: list[str] = []
559
+ for channel in channels:
560
+ WorkerDispatcher._require_state_load(flow, source, channel, Channel)
561
+ mapped_channels.append(channel.name)
562
+ mapped_channel_maps: list[str] = []
563
+ for channel_map in channel_maps:
564
+ WorkerDispatcher._require_state_load(flow, source, channel_map, ChannelMap)
565
+ mapped_channel_maps.append(f"{channel_map.name}/")
566
+ for channel_load in channel_map_instances:
567
+ if not isinstance(channel_load, ChannelMapLoad):
568
+ raise TypeError(f"{source} has an invalid ChannelMap instance load")
569
+ WorkerDispatcher._require_state_load(
570
+ flow, source, channel_load.channel_map, ChannelMap
571
+ )
572
+ mapped_channel_maps.append(channel_load.physical_name)
573
+ for kind, values in (
574
+ ("AttributeMap", mapped_attribute_maps),
575
+ ("Channel", mapped_channels),
576
+ ("ChannelMap", mapped_channel_maps),
577
+ ):
578
+ if len(values) != len(set(values)):
579
+ raise ValueError(f"{source} has a duplicate {kind} load")
580
+ return (
581
+ tuple(sorted(mapped_attribute_maps)),
582
+ tuple(sorted(mapped_channels)),
583
+ tuple(sorted(mapped_channel_maps)),
584
+ )
585
+
586
+ @staticmethod
587
+ def _require_state_load(
588
+ flow: _RegisteredFlow,
589
+ source: str,
590
+ definition: object,
591
+ expected_type: type[object],
592
+ ) -> None:
593
+ if not isinstance(definition, expected_type):
594
+ raise TypeError(f"{source} has an invalid state load")
595
+ if flow.persistence.get(cast(Any, definition).name) is not definition:
596
+ raise ValueError(f"{source} state load does not belong to Flow {flow.name}")
597
+
407
598
  def _map_wait(
408
599
  self,
409
600
  flow: _RegisteredFlow,
@@ -467,6 +658,14 @@ class WorkerDispatcher:
467
658
  FlowTimeoutPolicy.CANCEL: pb.FLOW_TIMEOUT_POLICY_CANCEL,
468
659
  FlowTimeoutPolicy.HANDLER: pb.FLOW_TIMEOUT_POLICY_HANDLER,
469
660
  }[timeout_policy]
661
+ timeout_handler_options = self.map_flow_timeout_handler_options(
662
+ target,
663
+ options.timeout,
664
+ timeout_policy,
665
+ options.timeout_handler_options,
666
+ )
667
+ if timeout_handler_options is not None:
668
+ mapped.timeout_handler_options.CopyFrom(timeout_handler_options)
470
669
  if options.start_delay is not None:
471
670
  mapped.flow_start_delay_seconds = self._seconds32(options.start_delay)
472
671
  if options.retry_policy is not None: