dex-python-sdk 0.1.3__tar.gz → 0.1.4__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 (60) hide show
  1. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/PKG-INFO +38 -18
  2. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/README.md +37 -17
  3. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/__init__.py +5 -2
  4. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/_grpc_errors.py +3 -3
  5. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/_worker_dispatcher.py +20 -7
  6. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/async_client.py +104 -45
  7. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/client.py +104 -44
  8. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/codec.py +15 -13
  9. dex_python_sdk-0.1.4/dex/dexpb/dex_pb2.py +409 -0
  10. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/dexpb/dex_pb2.pyi +113 -30
  11. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/dexpb/dex_pb2_grpc.py +21 -18
  12. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/flow_options.py +3 -5
  13. dex_python_sdk-0.1.4/dex/flow_result.py +127 -0
  14. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/runtime_errors.py +9 -29
  15. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/step.py +15 -6
  16. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/pyproject.toml +1 -1
  17. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/sdk-rust/Cargo.toml +3 -1
  18. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/sdk-rust/crates/dex-blob-cache/Cargo.toml +1 -0
  19. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/sdk-rust/crates/dex-blob-cache-python/Cargo.toml +2 -1
  20. dex_python_sdk-0.1.3/dex/dexpb/dex_pb2.py +0 -395
  21. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/LEGACY_NOTICES.md +0 -0
  22. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/LICENSE +0 -0
  23. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/_async_value_hydrator.py +0 -0
  24. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/_async_worker_dispatcher.py +0 -0
  25. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/_async_worker_service.py +0 -0
  26. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/_invocation_context.py +0 -0
  27. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/_native.pyi +0 -0
  28. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/_utils.py +0 -0
  29. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/_value_hydrator.py +0 -0
  30. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/_value_mapper.py +0 -0
  31. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/_worker_service.py +0 -0
  32. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/async_worker.py +0 -0
  33. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/attribute.py +0 -0
  34. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/blob_cache.py +0 -0
  35. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/channel.py +0 -0
  36. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/client_options.py +0 -0
  37. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/condition.py +0 -0
  38. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/context.py +0 -0
  39. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/dexpb/__init__.py +0 -0
  40. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/flow.py +0 -0
  41. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/flow_config.py +0 -0
  42. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/flow_info.py +0 -0
  43. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/py.typed +0 -0
  44. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/step_execution.py +0 -0
  45. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/timer.py +0 -0
  46. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/wait.py +0 -0
  47. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/worker.py +0 -0
  48. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/dex/worker_options.py +0 -0
  49. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/sdk-rust/Cargo.lock +0 -0
  50. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/sdk-rust/crates/dex-blob-cache/LICENSE +0 -0
  51. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/sdk-rust/crates/dex-blob-cache/src/config.rs +0 -0
  52. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/sdk-rust/crates/dex-blob-cache/src/entry.rs +0 -0
  53. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/sdk-rust/crates/dex-blob-cache/src/error.rs +0 -0
  54. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/sdk-rust/crates/dex-blob-cache/src/format.rs +0 -0
  55. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/sdk-rust/crates/dex-blob-cache/src/lib.rs +0 -0
  56. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/sdk-rust/crates/dex-blob-cache/src/policy.rs +0 -0
  57. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/sdk-rust/crates/dex-blob-cache/src/store.rs +0 -0
  58. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/sdk-rust/crates/dex-blob-cache/tests/blob_cache_integration.rs +0 -0
  59. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/sdk-rust/crates/dex-blob-cache-python/LICENSE +0 -0
  60. {dex_python_sdk-0.1.3 → dex_python_sdk-0.1.4}/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.1.3
3
+ Version: 0.1.4
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
@@ -71,7 +71,7 @@ registry = dex.Registry((flow,))
71
71
  ```
72
72
 
73
73
  Registry derives codecs from declared Python types and handler annotations.
74
- Built-in scalar types and dataclasses need no codec arguments. Register an
74
+ Built-in primitive types and dataclasses need no codec arguments. Register an
75
75
  explicit codec only for a custom encoding or a type Registry cannot derive.
76
76
  `PersistenceSchema.of(...)` accepts attributes and channels together and
77
77
  partitions them by definition type.
@@ -131,6 +131,12 @@ Applications implement two generic interfaces from [`dex`](dex/):
131
131
  `StepOptions.wait_for_method_timeout` and `execute_method_timeout` bound the
132
132
  two handler calls. Timer and channel conditions determine how long a Step waits.
133
133
 
134
+ `wait_for_retry` and `execute_retry` limit one logical handler execution. With
135
+ `StepDurability.ASYNC`, local and fallback regular activities share maximum
136
+ attempts, total duration, and 1-based attempt numbers. Fallback starts
137
+ immediately; later regular retries continue the backoff sequence at the
138
+ cumulative attempt.
139
+
134
140
  `Registry` validates every Flow, Step, RPC signature, durable name, lock, and
135
141
  codec before Client or Worker startup. `Client` methods use these typed objects
136
142
  instead of raw Flow, Step, or RPC strings.
@@ -141,14 +147,33 @@ instead of raw Flow, Step, or RPC strings.
141
147
  `Wait.any_combination_of` must have a non-empty user ID; the same Condition
142
148
  instance may appear in multiple combinations.
143
149
 
144
- Both `Client` and `AsyncClient` provide `wait_for_attribute_equal` and
145
- `wait_for_attribute_map_equal`. They target the current run and accept only
146
- string, bool, int, or float wire values. JSON objects, bytes, and null fail
150
+ Both `Client` and `AsyncClient` provide singleton and AttributeMap-instance
151
+ overloads of `wait_for_attribute_equal`. They target the current run and accept
152
+ only string, bool, int, or float wire values. JSON objects, bytes, and null fail
147
153
  before transport. `AttributeMap.get_map_size/get_all_instance_keys` include
148
154
  buffered sets and deletes. The matching `ChannelMap` methods are RPC-only,
149
155
  include buffered publishes, and omit empty instances. Keys are decoded and
150
156
  sorted. Use `force_complete_if_channels_empty(...)` for conditional completion.
151
157
 
158
+ `Client.wait_for_flow` and `AsyncClient.wait_for_flow` return a
159
+ `WaitForFlowResult` after hydrating every output-bearing completion. Use
160
+ `single_output` only when the Flow contract produces exactly one output:
161
+
162
+ ```python
163
+ output = client.wait_for_flow(flow_id).single_output(OrderResult)
164
+
165
+ result = client.wait_for_flow(flow_id)
166
+ for completion in result.completions:
167
+ if completion.step_execution_id == expected_execution_id:
168
+ output = completion.decode(OrderResult)
169
+ ```
170
+
171
+ `completions` is an immutable tuple in server collection order. Parallel branch
172
+ order is not deterministic, so select by `step_type` or `step_execution_id`.
173
+ No-output Flows return an empty tuple; `single_output` raises `ValueError` for
174
+ zero or multiple completions. `FlowUncompletedError.completions` uses the same
175
+ hydrated model for partial outputs.
176
+
152
177
  ### Errors
153
178
 
154
179
  Client calls raise concrete `DexServiceError` subclasses. Existing-Flow reads
@@ -199,22 +224,17 @@ implemented. Python owns its gRPC transport; the native bridge is limited to
199
224
  the shared BlobCache. Design notes:
200
225
  [`docs/design/plan/python-sdk-async-apis.md`](../docs/design/plan/python-sdk-async-apis.md).
201
226
 
202
- ## Running dex-server locally
227
+ ## Running Dex locally
203
228
 
204
- ### Option 1: use docker compose
205
- See [dex README](https://github.com/superdurable/dex#using-docker-image--docker-compose)
229
+ Install and start the complete local environment with `dexcli`:
206
230
 
207
- ### Option 2: VSCode Dev Container
231
+ ```bash
232
+ brew install superdurable/tap/dexcli
233
+ dexcli dev
234
+ ```
208
235
 
209
- Dev Container is an easy way to get dex-server running locally. Follow these steps to launch a dev container:
210
- - Install Docker, VSCode, and [VSCode Dev Container plugin](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers).
211
- - Open the project in VSCode.
212
- ```bash
213
- cd dex-python-sdk
214
- code .
215
- ```
216
- - Launch the Remote-Containers: Reopen in Container command from Command Palette (Ctrl + Shift + P). You can also click in the bottom left corner to access the remote container menu.
217
- - Once the dev container starts, dex-server will be listening on port 8801.
236
+ Dex Server listens on `127.0.0.1:8801`. See the
237
+ [CLI README](../cli/README.md) for endpoints and persistence options.
218
238
 
219
239
  ## How To Contribute
220
240
 
@@ -55,7 +55,7 @@ registry = dex.Registry((flow,))
55
55
  ```
56
56
 
57
57
  Registry derives codecs from declared Python types and handler annotations.
58
- Built-in scalar types and dataclasses need no codec arguments. Register an
58
+ Built-in primitive types and dataclasses need no codec arguments. Register an
59
59
  explicit codec only for a custom encoding or a type Registry cannot derive.
60
60
  `PersistenceSchema.of(...)` accepts attributes and channels together and
61
61
  partitions them by definition type.
@@ -115,6 +115,12 @@ Applications implement two generic interfaces from [`dex`](dex/):
115
115
  `StepOptions.wait_for_method_timeout` and `execute_method_timeout` bound the
116
116
  two handler calls. Timer and channel conditions determine how long a Step waits.
117
117
 
118
+ `wait_for_retry` and `execute_retry` limit one logical handler execution. With
119
+ `StepDurability.ASYNC`, local and fallback regular activities share maximum
120
+ attempts, total duration, and 1-based attempt numbers. Fallback starts
121
+ immediately; later regular retries continue the backoff sequence at the
122
+ cumulative attempt.
123
+
118
124
  `Registry` validates every Flow, Step, RPC signature, durable name, lock, and
119
125
  codec before Client or Worker startup. `Client` methods use these typed objects
120
126
  instead of raw Flow, Step, or RPC strings.
@@ -125,14 +131,33 @@ instead of raw Flow, Step, or RPC strings.
125
131
  `Wait.any_combination_of` must have a non-empty user ID; the same Condition
126
132
  instance may appear in multiple combinations.
127
133
 
128
- Both `Client` and `AsyncClient` provide `wait_for_attribute_equal` and
129
- `wait_for_attribute_map_equal`. They target the current run and accept only
130
- string, bool, int, or float wire values. JSON objects, bytes, and null fail
134
+ Both `Client` and `AsyncClient` provide singleton and AttributeMap-instance
135
+ overloads of `wait_for_attribute_equal`. They target the current run and accept
136
+ only string, bool, int, or float wire values. JSON objects, bytes, and null fail
131
137
  before transport. `AttributeMap.get_map_size/get_all_instance_keys` include
132
138
  buffered sets and deletes. The matching `ChannelMap` methods are RPC-only,
133
139
  include buffered publishes, and omit empty instances. Keys are decoded and
134
140
  sorted. Use `force_complete_if_channels_empty(...)` for conditional completion.
135
141
 
142
+ `Client.wait_for_flow` and `AsyncClient.wait_for_flow` return a
143
+ `WaitForFlowResult` after hydrating every output-bearing completion. Use
144
+ `single_output` only when the Flow contract produces exactly one output:
145
+
146
+ ```python
147
+ output = client.wait_for_flow(flow_id).single_output(OrderResult)
148
+
149
+ result = client.wait_for_flow(flow_id)
150
+ for completion in result.completions:
151
+ if completion.step_execution_id == expected_execution_id:
152
+ output = completion.decode(OrderResult)
153
+ ```
154
+
155
+ `completions` is an immutable tuple in server collection order. Parallel branch
156
+ order is not deterministic, so select by `step_type` or `step_execution_id`.
157
+ No-output Flows return an empty tuple; `single_output` raises `ValueError` for
158
+ zero or multiple completions. `FlowUncompletedError.completions` uses the same
159
+ hydrated model for partial outputs.
160
+
136
161
  ### Errors
137
162
 
138
163
  Client calls raise concrete `DexServiceError` subclasses. Existing-Flow reads
@@ -183,22 +208,17 @@ implemented. Python owns its gRPC transport; the native bridge is limited to
183
208
  the shared BlobCache. Design notes:
184
209
  [`docs/design/plan/python-sdk-async-apis.md`](../docs/design/plan/python-sdk-async-apis.md).
185
210
 
186
- ## Running dex-server locally
211
+ ## Running Dex locally
187
212
 
188
- ### Option 1: use docker compose
189
- See [dex README](https://github.com/superdurable/dex#using-docker-image--docker-compose)
213
+ Install and start the complete local environment with `dexcli`:
190
214
 
191
- ### Option 2: VSCode Dev Container
215
+ ```bash
216
+ brew install superdurable/tap/dexcli
217
+ dexcli dev
218
+ ```
192
219
 
193
- Dev Container is an easy way to get dex-server running locally. Follow these steps to launch a dev container:
194
- - Install Docker, VSCode, and [VSCode Dev Container plugin](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers).
195
- - Open the project in VSCode.
196
- ```bash
197
- cd dex-python-sdk
198
- code .
199
- ```
200
- - Launch the Remote-Containers: Reopen in Container command from Command Palette (Ctrl + Shift + P). You can also click in the bottom left corner to access the remote container menu.
201
- - Once the dev container starts, dex-server will be listening on port 8801.
220
+ Dex Server listens on `127.0.0.1:8801`. See the
221
+ [CLI README](../cli/README.md) for endpoints and persistence options.
202
222
 
203
223
  ## How To Contribute
204
224
 
@@ -8,6 +8,8 @@
8
8
  # Legacy Materials remain under their original licenses.
9
9
  # See LICENSE and LEGACY_NOTICES.md.
10
10
 
11
+ from dex.async_client import AsyncClient
12
+ from dex.async_worker import AsyncWorker
11
13
  from dex.attribute import (
12
14
  Attribute,
13
15
  AttributeIndex,
@@ -17,8 +19,6 @@ from dex.attribute import (
17
19
  )
18
20
  from dex.blob_cache import BlobCache, BlobCacheConfig, open_blob_cache
19
21
  from dex.channel import Channel, ChannelMap
20
- from dex.async_client import AsyncClient
21
- from dex.async_worker import AsyncWorker
22
22
  from dex.client import Client
23
23
  from dex.client_options import ClientOptions
24
24
  from dex.codec import (
@@ -52,6 +52,7 @@ from dex.flow_options import (
52
52
  StopFlowOptions,
53
53
  StopType,
54
54
  )
55
+ from dex.flow_result import StepCompletion, WaitForFlowResult
55
56
  from dex.runtime_errors import (
56
57
  DexServiceError,
57
58
  ErrorSubStatus,
@@ -140,6 +141,7 @@ __all__ = [
140
141
  "RetryPolicy",
141
142
  "SearchFlowEntry",
142
143
  "SearchFlowsPage",
144
+ "StepCompletion",
143
145
  "StartFlowOptions",
144
146
  "StepExecutionId",
145
147
  "StepDecision",
@@ -156,6 +158,7 @@ __all__ = [
156
158
  "ValueMappingError",
157
159
  "Wait",
158
160
  "WaitForFailurePolicy",
161
+ "WaitForFlowResult",
159
162
  "WireKind",
160
163
  "Worker",
161
164
  "WorkerInvocationError",
@@ -36,12 +36,12 @@ def translate_rpc_error(
36
36
  flow_id: str | None,
37
37
  requirement: FlowTargetRequirement,
38
38
  ) -> DexServiceError:
39
- details: pb.ErrorResponse | None = None
39
+ details: pb.ServiceErrorResponse | None = None
40
40
  try:
41
41
  status = rpc_status.from_call(cast(grpc.Call, error))
42
42
  if status is not None:
43
43
  for packed in status.details:
44
- candidate = pb.ErrorResponse()
44
+ candidate = pb.ServiceErrorResponse()
45
45
  if packed.Is(candidate.DESCRIPTOR):
46
46
  packed.Unpack(candidate)
47
47
  details = candidate
@@ -87,7 +87,7 @@ def translate_rpc_error(
87
87
  return DexServiceError(*parameters)
88
88
 
89
89
 
90
- def _worker_code(details: pb.ErrorResponse | None) -> grpc.StatusCode | None:
90
+ def _worker_code(details: pb.ServiceErrorResponse | None) -> grpc.StatusCode | None:
91
91
  if details is None or details.original_worker_error_status == 0:
92
92
  return None
93
93
  return next(
@@ -279,11 +279,19 @@ class WorkerDispatcher:
279
279
  mapped.next_steps.extend(self._map_movements(flow, decision.movements))
280
280
  elif decision.kind is DecisionKind.GRACEFUL_COMPLETE:
281
281
  mapped.close_decision.CopyFrom(
282
- self._close(pb.CLOSE_DECISION_TYPE_GRACEFUL_COMPLETE, decision.output)
282
+ self._close(
283
+ pb.CLOSE_DECISION_TYPE_GRACEFUL_COMPLETE,
284
+ decision.output,
285
+ decision._has_output(),
286
+ )
283
287
  )
284
288
  elif decision.kind is DecisionKind.FORCE_COMPLETE:
285
289
  mapped.close_decision.CopyFrom(
286
- self._close(pb.CLOSE_DECISION_TYPE_FORCE_COMPLETE, decision.output)
290
+ self._close(
291
+ pb.CLOSE_DECISION_TYPE_FORCE_COMPLETE,
292
+ decision.output,
293
+ decision._has_output(),
294
+ )
287
295
  )
288
296
  elif decision.kind is DecisionKind.FORCE_FAIL:
289
297
  mapped.close_decision.CopyFrom(
@@ -314,11 +322,16 @@ class WorkerDispatcher:
314
322
  raise ValueError("unsupported StepDecision kind")
315
323
  return mapped
316
324
 
317
- def _close(self, close_type: int, output: object) -> pb.CloseDecision:
318
- return pb.CloseDecision(
319
- close_decision_type=cast(Any, close_type),
320
- close_input=self._values.encode_dynamic(output),
321
- )
325
+ def _close(
326
+ self,
327
+ close_type: int,
328
+ output: object,
329
+ has_output: bool = True,
330
+ ) -> pb.CloseDecision:
331
+ close = pb.CloseDecision(close_decision_type=cast(Any, close_type))
332
+ if has_output:
333
+ close.close_input.CopyFrom(self._values.encode_dynamic(output))
334
+ return close
322
335
 
323
336
  def _map_movements(
324
337
  self,
@@ -40,10 +40,8 @@ from dex.flow_options import (
40
40
  StopFlowOptions,
41
41
  StopType,
42
42
  )
43
- from dex.runtime_errors import (
44
- FlowErrorType,
45
- FlowUncompletedError,
46
- )
43
+ from dex.flow_result import StepCompletion, WaitForFlowResult
44
+ from dex.runtime_errors import FlowErrorType, FlowUncompletedError
47
45
  from dex.step import RetryPolicy, StepDurability
48
46
  from dex.step_execution import StepExecutionId, TimerId
49
47
 
@@ -67,7 +65,7 @@ class AsyncClient:
67
65
  Examples:
68
66
  >>> async with AsyncClient(registry, cache) as client:
69
67
  ... run_id = await client.start_flow(orders, "order-42", input)
70
- ... result = await client.wait_for_flow("order-42", OrderResult)
68
+ ... result = (await client.wait_for_flow("order-42")).single_output(OrderResult)
71
69
  """
72
70
 
73
71
  def __init__(
@@ -500,54 +498,44 @@ class AsyncClient:
500
498
  "active",
501
499
  )
502
500
 
503
- @overload
504
- async def wait_for_flow(self, flow_id: str) -> None: ...
505
-
506
- @overload
507
- async def wait_for_flow(
508
- self,
509
- flow_id: str,
510
- output_type: type[OutputT],
511
- timeout: timedelta | None = None,
512
- ) -> OutputT: ...
513
-
514
501
  async def wait_for_flow(
515
502
  self,
516
503
  flow_id: str,
517
- output_type: type[Any] | None = None,
518
504
  timeout: timedelta | None = None,
519
- ) -> Any:
520
- """Await Flow closure and optionally decode its latest Step output.
505
+ ) -> WaitForFlowResult:
506
+ """Await Flow closure and return all output-bearing completions.
521
507
 
522
508
  ``timeout`` is a server-side long-poll duration, not a local task deadline.
523
- A LongPollTimeoutError is retryable by awaiting this method again.
509
+ A LongPollTimeoutError is retryable by awaiting this method again. Parallel
510
+ completion order is not deterministic; select results by Step identity.
524
511
 
525
512
  Args:
526
513
  flow_id: The non-empty target Flow ID.
527
- output_type: Optional Python type for the latest completed Step output.
528
514
  timeout: Optional non-negative server-side wait duration.
529
515
 
530
516
  Returns:
531
- The decoded latest output, or ``None`` when no output type/value exists.
517
+ An immutable snapshot of every output-bearing Step completion.
532
518
 
533
519
  Raises:
534
520
  LongPollTimeoutError: If the Flow remains open when the wait expires.
535
521
  FlowUncompletedError: If the Flow closes without successful completion.
536
522
  FlowNotFoundError: If ``flow_id`` does not exist.
537
- ValueMappingError: If a requested output cannot be decoded.
523
+ ValueMappingError: If Dex returns a malformed or incompatible hydrated value.
538
524
  DexServiceError: If FlowService cannot perform the wait.
539
525
  """
540
526
  response = await self._wait_for_flow_response(flow_id, timeout)
541
- if output_type is None:
542
- return None
543
- codec = self._values.codec(output_type)
544
- for result in reversed(response.results):
545
- if result.HasField("completed_step_output"):
546
- return self._values.decode(
547
- await self._hydrator.hydrate(result.completed_step_output),
548
- codec,
527
+ hydrated = await self._hydrator.step_outputs(list(response.results))
528
+ return WaitForFlowResult(
529
+ [
530
+ StepCompletion(
531
+ result,
532
+ lambda value, output_type: self._values.decode(
533
+ value, self._values.codec(output_type)
534
+ ),
549
535
  )
550
- return None
536
+ for result in hydrated
537
+ ]
538
+ )
551
539
 
552
540
  async def stop_flow(
553
541
  self,
@@ -700,8 +688,7 @@ class AsyncClient:
700
688
  ResetType.STEP_EXECUTION_ID: pb.FLOW_RESET_TYPE_STEP_EXECUTION_ID,
701
689
  }[options.type],
702
690
  reason=options.reason or "",
703
- skip_channel_messages_reapply=options.skip_channel_messages_reapply,
704
- skip_locking_rpc_reapply=options.skip_locking_rpc_reapply,
691
+ skip_writes_reapply=options.skip_writes_reapply,
705
692
  )
706
693
  if options.history_event_id is not None:
707
694
  request.history_event_id = options.history_event_id
@@ -787,6 +774,7 @@ class AsyncClient:
787
774
  "active",
788
775
  )
789
776
 
777
+ @overload
790
778
  async def wait_for_attribute_equal(
791
779
  self,
792
780
  flow_id: str,
@@ -794,7 +782,7 @@ class AsyncClient:
794
782
  expected: ValueT,
795
783
  timeout: timedelta,
796
784
  ) -> None:
797
- """Await a scalar Attribute in the current run equaling ``expected``.
785
+ """Await a singleton Attribute in the current run equaling ``expected``.
798
786
 
799
787
  The Client generates a request ID. JSON, bytes, and null values raise
800
788
  ``ValueError`` before transport. A remote expiry raises
@@ -802,7 +790,7 @@ class AsyncClient:
802
790
 
803
791
  Args:
804
792
  flow_id: The non-empty active Flow ID.
805
- attribute: The registered scalar Attribute to observe.
793
+ attribute: The registered singleton Attribute to observe.
806
794
  expected: The string, bool, int, or float value to await.
807
795
  timeout: The non-negative server-side wait duration.
808
796
 
@@ -812,11 +800,10 @@ class AsyncClient:
812
800
  FlowNotActiveError: If the Flow closes first.
813
801
  DexServiceError: If FlowService cannot perform the wait.
814
802
  """
815
- await self._wait_for_attribute_equal(
816
- flow_id, attribute, None, expected, timeout
817
- )
803
+ ...
818
804
 
819
- async def wait_for_attribute_map_equal(
805
+ @overload
806
+ async def wait_for_attribute_equal(
820
807
  self,
821
808
  flow_id: str,
822
809
  attribute: AttributeMap[ValueT],
@@ -824,9 +811,9 @@ class AsyncClient:
824
811
  expected: ValueT,
825
812
  timeout: timedelta,
826
813
  ) -> None:
827
- """Await one scalar AttributeMap instance in the current run.
814
+ """Await one AttributeMap instance in the current run.
828
815
 
829
- Scalar restrictions, request-ID generation, timeout behavior, and
816
+ Primitive-value restrictions, request-ID generation, timeout behavior, and
830
817
  service errors match :meth:`wait_for_attribute_equal`.
831
818
 
832
819
  Args:
@@ -842,6 +829,36 @@ class AsyncClient:
842
829
  FlowNotActiveError: If the Flow closes first.
843
830
  DexServiceError: If FlowService cannot perform the wait.
844
831
  """
832
+ ...
833
+
834
+ async def wait_for_attribute_equal(
835
+ self,
836
+ flow_id: str,
837
+ attribute: Attribute[Any] | AttributeMap[Any],
838
+ *args: object,
839
+ **kwargs: object,
840
+ ) -> None:
841
+ """Await a singleton Attribute or AttributeMap instance equaling a value.
842
+
843
+ Singleton form is ``wait_for_attribute_equal(flow_id, attribute, expected,
844
+ timeout)``; map form adds ``instance`` before ``expected``.
845
+
846
+ Args:
847
+ flow_id: The non-empty active Flow ID.
848
+ attribute: The registered Attribute or AttributeMap to observe.
849
+ *args: Positional expected value and timeout, optionally preceded by a map instance.
850
+ **kwargs: The same arguments supplied by name.
851
+
852
+ Raises:
853
+ TypeError: If arguments do not match the Attribute definition.
854
+ ValueError: If an identifier, timeout, or expected value is invalid.
855
+ LongPollTimeoutError: If equality is not observed before the timeout.
856
+ FlowNotActiveError: If the Flow closes first.
857
+ DexServiceError: If FlowService cannot perform the wait.
858
+ """
859
+ instance, expected, timeout = self._attribute_wait_arguments(
860
+ attribute, args, kwargs
861
+ )
845
862
  await self._wait_for_attribute_equal(
846
863
  flow_id, attribute, instance, expected, timeout
847
864
  )
@@ -864,7 +881,9 @@ class AsyncClient:
864
881
  "int_value",
865
882
  "double_value",
866
883
  }:
867
- raise ValueError("wait_for_attribute_equal supports only scalar values")
884
+ raise ValueError(
885
+ "wait_for_attribute_equal supports only string, boolean, or number values"
886
+ )
868
887
  await self._call(
869
888
  self._service.WaitForAttribute,
870
889
  pb.WaitForAttributeRequest(
@@ -981,13 +1000,21 @@ class AsyncClient:
981
1000
  if response.flow_status != pb.FLOW_STATUS_COMPLETED:
982
1001
  info = await self.describe_flow(flow_id)
983
1002
  results = await self._hydrator.step_outputs(list(response.results))
1003
+ completions = [
1004
+ StepCompletion(
1005
+ result,
1006
+ lambda value, output_type: self._values.decode(
1007
+ value, self._values.codec(output_type)
1008
+ ),
1009
+ )
1010
+ for result in results
1011
+ ]
984
1012
  raise FlowUncompletedError(
985
1013
  info.run_id,
986
1014
  self._map_flow_status(response.flow_status),
987
1015
  self._map_flow_error_type(response.error_type),
988
1016
  response.error_message or None,
989
- results,
990
- self._values,
1017
+ completions,
991
1018
  )
992
1019
  return response
993
1020
 
@@ -1126,6 +1153,38 @@ class AsyncClient:
1126
1153
  return args[0], args[1]
1127
1154
  raise TypeError("set_attribute received invalid arguments")
1128
1155
 
1156
+ @staticmethod
1157
+ def _attribute_wait_arguments(
1158
+ definition: Attribute[Any] | AttributeMap[Any],
1159
+ args: tuple[object, ...],
1160
+ kwargs: dict[str, object],
1161
+ ) -> tuple[str | None, object, timedelta]:
1162
+ parameter_names: tuple[str, ...]
1163
+ if isinstance(definition, Attribute):
1164
+ parameter_names = ("expected", "timeout")
1165
+ elif isinstance(definition, AttributeMap):
1166
+ parameter_names = ("instance", "expected", "timeout")
1167
+ else:
1168
+ raise TypeError("wait_for_attribute_equal received invalid arguments")
1169
+ if len(args) > len(parameter_names):
1170
+ raise TypeError("wait_for_attribute_equal received invalid arguments")
1171
+ arguments = dict(zip(parameter_names, args))
1172
+ for name, value in kwargs.items():
1173
+ if name not in parameter_names or name in arguments:
1174
+ raise TypeError("wait_for_attribute_equal received invalid arguments")
1175
+ arguments[name] = value
1176
+ if set(arguments) != set(parameter_names):
1177
+ raise TypeError("wait_for_attribute_equal received invalid arguments")
1178
+ timeout = arguments["timeout"]
1179
+ if not isinstance(timeout, timedelta):
1180
+ raise TypeError("wait_for_attribute_equal received invalid arguments")
1181
+ if isinstance(definition, Attribute):
1182
+ return None, arguments["expected"], timeout
1183
+ instance = arguments["instance"]
1184
+ if not isinstance(instance, str):
1185
+ raise TypeError("wait_for_attribute_equal received invalid arguments")
1186
+ return instance, arguments["expected"], timeout
1187
+
1129
1188
  @staticmethod
1130
1189
  def _seconds32(duration: timedelta) -> int:
1131
1190
  seconds = duration.total_seconds()