dex-python-sdk 0.1.4__tar.gz → 0.1.5__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.1.4 → dex_python_sdk-0.1.5}/PKG-INFO +81 -5
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/README.md +80 -4
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/__init__.py +16 -8
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/_async_value_hydrator.py +4 -2
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/_async_worker_dispatcher.py +62 -3
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/_async_worker_service.py +44 -11
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/_invocation_context.py +31 -1
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/_value_hydrator.py +11 -2
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/_worker_dispatcher.py +254 -5
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/_worker_service.py +17 -3
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/async_client.py +58 -53
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/client.py +58 -53
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/condition.py +21 -1
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/context.py +13 -1
- dex_python_sdk-0.1.5/dex/dexpb/dex_pb2.py +441 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/dexpb/dex_pb2.pyi +212 -49
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/dexpb/dex_pb2_grpc.py +3 -3
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/flow.py +65 -2
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/flow_info.py +2 -2
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/flow_options.py +152 -12
- dex_python_sdk-0.1.5/dex/flow_result.py +215 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/runtime_errors.py +3 -44
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/step.py +63 -0
- dex_python_sdk-0.1.5/dex/subflow.py +85 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/pyproject.toml +1 -1
- dex_python_sdk-0.1.4/dex/dexpb/dex_pb2.py +0 -409
- dex_python_sdk-0.1.4/dex/flow_result.py +0 -127
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/LEGACY_NOTICES.md +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/LICENSE +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/_grpc_errors.py +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/_native.pyi +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/_utils.py +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/_value_mapper.py +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/async_worker.py +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/attribute.py +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/blob_cache.py +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/channel.py +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/client_options.py +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/codec.py +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/dexpb/__init__.py +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/flow_config.py +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/py.typed +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/step_execution.py +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/timer.py +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/wait.py +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/worker.py +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/dex/worker_options.py +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/sdk-rust/Cargo.lock +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/sdk-rust/Cargo.toml +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/sdk-rust/crates/dex-blob-cache/Cargo.toml +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/sdk-rust/crates/dex-blob-cache/LICENSE +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/sdk-rust/crates/dex-blob-cache/src/config.rs +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/sdk-rust/crates/dex-blob-cache/src/entry.rs +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/sdk-rust/crates/dex-blob-cache/src/error.rs +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/sdk-rust/crates/dex-blob-cache/src/format.rs +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/sdk-rust/crates/dex-blob-cache/src/lib.rs +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/sdk-rust/crates/dex-blob-cache/src/policy.rs +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/sdk-rust/crates/dex-blob-cache/src/store.rs +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/sdk-rust/crates/dex-blob-cache/tests/blob_cache_integration.rs +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/sdk-rust/crates/dex-blob-cache-python/Cargo.toml +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/sdk-rust/crates/dex-blob-cache-python/LICENSE +0 -0
- {dex_python_sdk-0.1.4 → dex_python_sdk-0.1.5}/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
|
+
Version: 0.1.5
|
|
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
|
|
@@ -137,6 +137,65 @@ attempts, total duration, and 1-based attempt numbers. Fallback starts
|
|
|
137
137
|
immediately; later regular retries continue the backoff sequence at the
|
|
138
138
|
cumulative attempt.
|
|
139
139
|
|
|
140
|
+
### Canceling Step executions
|
|
141
|
+
|
|
142
|
+
A successful Step can cancel queued or active executions while continuing with
|
|
143
|
+
its normal decision:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
return (
|
|
147
|
+
dex.go_to(self.record_quote, quote)
|
|
148
|
+
.with_canceling_sibling_steps(self.carrier_a, self.carrier_b)
|
|
149
|
+
.with_canceling_steps(self.global_quote_timeout)
|
|
150
|
+
)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`with_canceling_steps` selects every current execution of each registered Step
|
|
154
|
+
type. `with_canceling_sibling_steps` selects only executions with the same
|
|
155
|
+
`Context.from_step_execution_id` as the current execution. Decisions are
|
|
156
|
+
immutable; repeated calls form a union, and Flow-wide selection wins for the
|
|
157
|
+
same Step type. Unregistered selectors produce an invalid Step result.
|
|
158
|
+
|
|
159
|
+
Dex resolves one snapshot after the current execution succeeds. Completed,
|
|
160
|
+
already-canceled, and absent targets are no-ops. Next Steps created by the same
|
|
161
|
+
decision are outside the snapshot. Dex immediately applies the next or close
|
|
162
|
+
action; late decisions, writes, retries, and recovery Steps are discarded.
|
|
163
|
+
|
|
164
|
+
Set `StepOptions.heartbeat_timeout` on long-running regular Steps so
|
|
165
|
+
cancellation reaches the Worker promptly. It applies to `wait_for` and
|
|
166
|
+
`execute`; local activities ignore it, while an ASYNC fallback uses it. `None`
|
|
167
|
+
and zero disable heartbeats, and positive values must be whole seconds in the
|
|
168
|
+
signed int32 range. `AsyncWorker` cancels the handler's asyncio task. A handler
|
|
169
|
+
may catch `asyncio.CancelledError` for cleanup; synchronous CPU-bound handlers
|
|
170
|
+
may check `Context.is_cancellation_requested()` at natural boundaries.
|
|
171
|
+
|
|
172
|
+
`RPCResult.with_canceling_steps` provides the Flow-wide selector for RPCs.
|
|
173
|
+
RPCs do not support sibling selection because they have no Step execution
|
|
174
|
+
lineage.
|
|
175
|
+
|
|
176
|
+
### Soft Flow timeout
|
|
177
|
+
|
|
178
|
+
Override `Flow.handle_timeout` to make a positive timeout use handler policy
|
|
179
|
+
by default. Both synchronous and async Workers support the hook:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
class Orders(dex.Flow[str]):
|
|
183
|
+
async def handle_timeout(self, context: dex.Context) -> dex.StepDecision:
|
|
184
|
+
await notify_expiration(context)
|
|
185
|
+
return dex.force_complete("expired")
|
|
186
|
+
|
|
187
|
+
options = dex.StartFlowOptions(
|
|
188
|
+
timeout=timedelta(minutes=30),
|
|
189
|
+
timeout_policy=dex.FlowTimeoutPolicy.HANDLER,
|
|
190
|
+
)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Register async hooks with `allow_async_handlers=True` and run them with
|
|
194
|
+
`AsyncWorker`. `FAIL` produces `FlowErrorType.FLOW_TIMEOUT` and permits Flow
|
|
195
|
+
retry; `CANCEL` cancels without retry. Continue-as-new preserves the deadline,
|
|
196
|
+
while retry and cron runs receive a fresh budget. A zero or absent timeout
|
|
197
|
+
disables the feature.
|
|
198
|
+
|
|
140
199
|
`Registry` validates every Flow, Step, RPC signature, durable name, lock, and
|
|
141
200
|
codec before Client or Worker startup. `Client` methods use these typed objects
|
|
142
201
|
instead of raw Flow, Step, or RPC strings.
|
|
@@ -156,7 +215,7 @@ include buffered publishes, and omit empty instances. Keys are decoded and
|
|
|
156
215
|
sorted. Use `force_complete_if_channels_empty(...)` for conditional completion.
|
|
157
216
|
|
|
158
217
|
`Client.wait_for_flow` and `AsyncClient.wait_for_flow` return a
|
|
159
|
-
`
|
|
218
|
+
`FlowResult` after hydrating every output-bearing completion. Use
|
|
160
219
|
`single_output` only when the Flow contract produces exactly one output:
|
|
161
220
|
|
|
162
221
|
```python
|
|
@@ -171,13 +230,30 @@ for completion in result.completions:
|
|
|
171
230
|
`completions` is an immutable tuple in server collection order. Parallel branch
|
|
172
231
|
order is not deterministic, so select by `step_type` or `step_execution_id`.
|
|
173
232
|
No-output Flows return an empty tuple; `single_output` raises `ValueError` for
|
|
174
|
-
zero or multiple completions.
|
|
175
|
-
|
|
233
|
+
zero or multiple completions. Every terminal status returns a `FlowResult`; inspect
|
|
234
|
+
`status`, `error_type`, and `error_message` for unsuccessful completion.
|
|
235
|
+
|
|
236
|
+
SubFlows are normal, independently addressable Flows used as durable Conditions:
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
def wait_for(self, context: Context, input: ChargeInput) -> Wait:
|
|
240
|
+
return Wait.until(SubFlow.run(self.charge_flow, input))
|
|
241
|
+
|
|
242
|
+
def execute(self, context: Context, input: ChargeInput) -> StepDecision:
|
|
243
|
+
del input
|
|
244
|
+
receipt = SubFlow.get_condition_results(context).single_output(Receipt)
|
|
245
|
+
return graceful_complete(receipt)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
`SubFlow.get_flow_id(context, index=0)` remains available for a running `any_of`
|
|
249
|
+
loser. `SubFlowOptions` configures timing, timeout policy, retry, initial target
|
|
250
|
+
Attributes, Flow config, Condition ID, and reuse. Parent completion does not cancel an unfinished
|
|
251
|
+
SubFlow.
|
|
176
252
|
|
|
177
253
|
### Errors
|
|
178
254
|
|
|
179
255
|
Client calls raise concrete `DexServiceError` subclasses. Existing-Flow reads
|
|
180
|
-
(`get_attribute`, `describe_flow`, `wait_for_flow`, and `
|
|
256
|
+
(`get_attribute`, `describe_flow`, `wait_for_flow`, and `time_travel`) raise
|
|
181
257
|
`FlowNotFoundError` when the Flow does not exist. Mutations, RPCs, timer/Step
|
|
182
258
|
waits, config updates, and continue-as-new triggers raise
|
|
183
259
|
`FlowNotActiveError` when no running Flow can accept the operation.
|
|
@@ -121,6 +121,65 @@ attempts, total duration, and 1-based attempt numbers. Fallback starts
|
|
|
121
121
|
immediately; later regular retries continue the backoff sequence at the
|
|
122
122
|
cumulative attempt.
|
|
123
123
|
|
|
124
|
+
### Canceling Step executions
|
|
125
|
+
|
|
126
|
+
A successful Step can cancel queued or active executions while continuing with
|
|
127
|
+
its normal decision:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
return (
|
|
131
|
+
dex.go_to(self.record_quote, quote)
|
|
132
|
+
.with_canceling_sibling_steps(self.carrier_a, self.carrier_b)
|
|
133
|
+
.with_canceling_steps(self.global_quote_timeout)
|
|
134
|
+
)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`with_canceling_steps` selects every current execution of each registered Step
|
|
138
|
+
type. `with_canceling_sibling_steps` selects only executions with the same
|
|
139
|
+
`Context.from_step_execution_id` as the current execution. Decisions are
|
|
140
|
+
immutable; repeated calls form a union, and Flow-wide selection wins for the
|
|
141
|
+
same Step type. Unregistered selectors produce an invalid Step result.
|
|
142
|
+
|
|
143
|
+
Dex resolves one snapshot after the current execution succeeds. Completed,
|
|
144
|
+
already-canceled, and absent targets are no-ops. Next Steps created by the same
|
|
145
|
+
decision are outside the snapshot. Dex immediately applies the next or close
|
|
146
|
+
action; late decisions, writes, retries, and recovery Steps are discarded.
|
|
147
|
+
|
|
148
|
+
Set `StepOptions.heartbeat_timeout` on long-running regular Steps so
|
|
149
|
+
cancellation reaches the Worker promptly. It applies to `wait_for` and
|
|
150
|
+
`execute`; local activities ignore it, while an ASYNC fallback uses it. `None`
|
|
151
|
+
and zero disable heartbeats, and positive values must be whole seconds in the
|
|
152
|
+
signed int32 range. `AsyncWorker` cancels the handler's asyncio task. A handler
|
|
153
|
+
may catch `asyncio.CancelledError` for cleanup; synchronous CPU-bound handlers
|
|
154
|
+
may check `Context.is_cancellation_requested()` at natural boundaries.
|
|
155
|
+
|
|
156
|
+
`RPCResult.with_canceling_steps` provides the Flow-wide selector for RPCs.
|
|
157
|
+
RPCs do not support sibling selection because they have no Step execution
|
|
158
|
+
lineage.
|
|
159
|
+
|
|
160
|
+
### Soft Flow timeout
|
|
161
|
+
|
|
162
|
+
Override `Flow.handle_timeout` to make a positive timeout use handler policy
|
|
163
|
+
by default. Both synchronous and async Workers support the hook:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
class Orders(dex.Flow[str]):
|
|
167
|
+
async def handle_timeout(self, context: dex.Context) -> dex.StepDecision:
|
|
168
|
+
await notify_expiration(context)
|
|
169
|
+
return dex.force_complete("expired")
|
|
170
|
+
|
|
171
|
+
options = dex.StartFlowOptions(
|
|
172
|
+
timeout=timedelta(minutes=30),
|
|
173
|
+
timeout_policy=dex.FlowTimeoutPolicy.HANDLER,
|
|
174
|
+
)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Register async hooks with `allow_async_handlers=True` and run them with
|
|
178
|
+
`AsyncWorker`. `FAIL` produces `FlowErrorType.FLOW_TIMEOUT` and permits Flow
|
|
179
|
+
retry; `CANCEL` cancels without retry. Continue-as-new preserves the deadline,
|
|
180
|
+
while retry and cron runs receive a fresh budget. A zero or absent timeout
|
|
181
|
+
disables the feature.
|
|
182
|
+
|
|
124
183
|
`Registry` validates every Flow, Step, RPC signature, durable name, lock, and
|
|
125
184
|
codec before Client or Worker startup. `Client` methods use these typed objects
|
|
126
185
|
instead of raw Flow, Step, or RPC strings.
|
|
@@ -140,7 +199,7 @@ include buffered publishes, and omit empty instances. Keys are decoded and
|
|
|
140
199
|
sorted. Use `force_complete_if_channels_empty(...)` for conditional completion.
|
|
141
200
|
|
|
142
201
|
`Client.wait_for_flow` and `AsyncClient.wait_for_flow` return a
|
|
143
|
-
`
|
|
202
|
+
`FlowResult` after hydrating every output-bearing completion. Use
|
|
144
203
|
`single_output` only when the Flow contract produces exactly one output:
|
|
145
204
|
|
|
146
205
|
```python
|
|
@@ -155,13 +214,30 @@ for completion in result.completions:
|
|
|
155
214
|
`completions` is an immutable tuple in server collection order. Parallel branch
|
|
156
215
|
order is not deterministic, so select by `step_type` or `step_execution_id`.
|
|
157
216
|
No-output Flows return an empty tuple; `single_output` raises `ValueError` for
|
|
158
|
-
zero or multiple completions.
|
|
159
|
-
|
|
217
|
+
zero or multiple completions. Every terminal status returns a `FlowResult`; inspect
|
|
218
|
+
`status`, `error_type`, and `error_message` for unsuccessful completion.
|
|
219
|
+
|
|
220
|
+
SubFlows are normal, independently addressable Flows used as durable Conditions:
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
def wait_for(self, context: Context, input: ChargeInput) -> Wait:
|
|
224
|
+
return Wait.until(SubFlow.run(self.charge_flow, input))
|
|
225
|
+
|
|
226
|
+
def execute(self, context: Context, input: ChargeInput) -> StepDecision:
|
|
227
|
+
del input
|
|
228
|
+
receipt = SubFlow.get_condition_results(context).single_output(Receipt)
|
|
229
|
+
return graceful_complete(receipt)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
`SubFlow.get_flow_id(context, index=0)` remains available for a running `any_of`
|
|
233
|
+
loser. `SubFlowOptions` configures timing, timeout policy, retry, initial target
|
|
234
|
+
Attributes, Flow config, Condition ID, and reuse. Parent completion does not cancel an unfinished
|
|
235
|
+
SubFlow.
|
|
160
236
|
|
|
161
237
|
### Errors
|
|
162
238
|
|
|
163
239
|
Client calls raise concrete `DexServiceError` subclasses. Existing-Flow reads
|
|
164
|
-
(`get_attribute`, `describe_flow`, `wait_for_flow`, and `
|
|
240
|
+
(`get_attribute`, `describe_flow`, `wait_for_flow`, and `time_travel`) raise
|
|
165
241
|
`FlowNotFoundError` when the Flow does not exist. Mutations, RPCs, timer/Step
|
|
166
242
|
waits, config updates, and continue-as-new triggers raise
|
|
167
243
|
`FlowNotActiveError` when no running Flow can accept the operation.
|
|
@@ -45,14 +45,18 @@ from dex.flow_info import (
|
|
|
45
45
|
SearchFlowsPage,
|
|
46
46
|
)
|
|
47
47
|
from dex.flow_options import (
|
|
48
|
+
FlowTimeoutPolicy,
|
|
48
49
|
IdReusePolicy,
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
TimeTravelOptions,
|
|
51
|
+
TimeTravelStepMethod,
|
|
52
|
+
TimeTravelType,
|
|
51
53
|
StartFlowOptions,
|
|
54
|
+
SubFlowOptions,
|
|
55
|
+
SubFlowReusePolicy,
|
|
52
56
|
StopFlowOptions,
|
|
53
57
|
StopType,
|
|
54
58
|
)
|
|
55
|
-
from dex.flow_result import
|
|
59
|
+
from dex.flow_result import FlowResult, StepCompletion
|
|
56
60
|
from dex.runtime_errors import (
|
|
57
61
|
DexServiceError,
|
|
58
62
|
ErrorSubStatus,
|
|
@@ -61,7 +65,6 @@ from dex.runtime_errors import (
|
|
|
61
65
|
FlowErrorType,
|
|
62
66
|
FlowNotActiveError,
|
|
63
67
|
FlowNotFoundError,
|
|
64
|
-
FlowUncompletedError,
|
|
65
68
|
InvalidStepResultError,
|
|
66
69
|
LongPollTimeoutError,
|
|
67
70
|
RpcLockConflictError,
|
|
@@ -87,6 +90,7 @@ from dex.step import (
|
|
|
87
90
|
)
|
|
88
91
|
from dex.step_execution import StepExecutionId, TimerId
|
|
89
92
|
from dex.timer import Timer
|
|
93
|
+
from dex.subflow import SubFlow
|
|
90
94
|
from dex.wait import Wait
|
|
91
95
|
from dex.worker import Worker
|
|
92
96
|
from dex.worker_options import WorkerOptions, WorkerTarget
|
|
@@ -122,10 +126,11 @@ __all__ = [
|
|
|
122
126
|
"FlowDefinitionError",
|
|
123
127
|
"FlowErrorType",
|
|
124
128
|
"FlowInfo",
|
|
129
|
+
"FlowResult",
|
|
125
130
|
"FlowNotActiveError",
|
|
126
131
|
"FlowNotFoundError",
|
|
127
132
|
"FlowStatus",
|
|
128
|
-
"
|
|
133
|
+
"FlowTimeoutPolicy",
|
|
129
134
|
"HealthInfo",
|
|
130
135
|
"IdReusePolicy",
|
|
131
136
|
"InvalidStepResultError",
|
|
@@ -136,13 +141,17 @@ __all__ = [
|
|
|
136
141
|
"RPCResult",
|
|
137
142
|
"RpcLockConflictError",
|
|
138
143
|
"Registry",
|
|
139
|
-
"
|
|
140
|
-
"
|
|
144
|
+
"TimeTravelOptions",
|
|
145
|
+
"TimeTravelStepMethod",
|
|
146
|
+
"TimeTravelType",
|
|
141
147
|
"RetryPolicy",
|
|
142
148
|
"SearchFlowEntry",
|
|
143
149
|
"SearchFlowsPage",
|
|
144
150
|
"StepCompletion",
|
|
145
151
|
"StartFlowOptions",
|
|
152
|
+
"SubFlow",
|
|
153
|
+
"SubFlowOptions",
|
|
154
|
+
"SubFlowReusePolicy",
|
|
146
155
|
"StepExecutionId",
|
|
147
156
|
"StepDecision",
|
|
148
157
|
"Step",
|
|
@@ -158,7 +167,6 @@ __all__ = [
|
|
|
158
167
|
"ValueMappingError",
|
|
159
168
|
"Wait",
|
|
160
169
|
"WaitForFailurePolicy",
|
|
161
|
-
"WaitForFlowResult",
|
|
162
170
|
"WireKind",
|
|
163
171
|
"Worker",
|
|
164
172
|
"WorkerInvocationError",
|
|
@@ -88,13 +88,15 @@ class AsyncValueHydrator:
|
|
|
88
88
|
) -> pb.InvokeExecuteMethodRequest:
|
|
89
89
|
result = pb.InvokeExecuteMethodRequest()
|
|
90
90
|
result.CopyFrom(request)
|
|
91
|
-
|
|
91
|
+
has_step_input = request.HasField("step_input")
|
|
92
|
+
values = [request.step_input] if has_step_input else []
|
|
92
93
|
values.extend(entry.value for entry in request.attributes)
|
|
93
94
|
values.extend(entry.value for entry in request.step_exe_locals)
|
|
94
95
|
for channel_result in request.condition_results.channel_results:
|
|
95
96
|
values.extend(channel_result.values)
|
|
96
97
|
hydrated = iter(await self.hydrate_all(values))
|
|
97
|
-
|
|
98
|
+
if has_step_input:
|
|
99
|
+
result.step_input.CopyFrom(next(hydrated))
|
|
98
100
|
for entry in result.attributes:
|
|
99
101
|
entry.value.CopyFrom(next(hydrated))
|
|
100
102
|
for entry in result.step_exe_locals:
|
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
from inspect import isawaitable
|
|
12
|
-
from typing import Any
|
|
12
|
+
from typing import Any, Callable
|
|
13
13
|
|
|
14
14
|
from dex._async_value_hydrator import AsyncValueHydrator
|
|
15
15
|
from dex._invocation_context import InvocationContext, InvocationMethod
|
|
16
16
|
from dex._value_mapper import ValueMapper
|
|
17
|
-
from dex._worker_dispatcher import WorkerDispatcher
|
|
17
|
+
from dex._worker_dispatcher import _TIMEOUT_HANDLER_STEP_TYPE, WorkerDispatcher
|
|
18
18
|
from dex.dexpb import dex_pb2 as pb
|
|
19
|
-
from dex.flow import
|
|
19
|
+
from dex.flow import Registry, RPCResult
|
|
20
20
|
from dex.runtime_errors import InvalidStepResultError, ValueMappingError
|
|
21
21
|
from dex.step import StepDecision
|
|
22
22
|
from dex.wait import Wait
|
|
@@ -36,6 +36,7 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
36
36
|
async def invoke_wait_for( # type: ignore[override]
|
|
37
37
|
self,
|
|
38
38
|
original: pb.InvokeWaitForMethodRequest,
|
|
39
|
+
is_active: Callable[[], bool] | None = None,
|
|
39
40
|
) -> pb.InvokeWaitForMethodResponse:
|
|
40
41
|
request = await self._async_hydrator.wait_for_request(original)
|
|
41
42
|
flow = self._registry._flow_by_type(request.flow_type)
|
|
@@ -46,6 +47,7 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
46
47
|
request.context,
|
|
47
48
|
self._values,
|
|
48
49
|
request.attributes,
|
|
50
|
+
is_active=is_active,
|
|
49
51
|
)
|
|
50
52
|
input = self._values.decode(request.step_input, step.input_codec)
|
|
51
53
|
wait = step.step.wait_for(context, input)
|
|
@@ -72,9 +74,12 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
72
74
|
async def invoke_execute( # type: ignore[override]
|
|
73
75
|
self,
|
|
74
76
|
original: pb.InvokeExecuteMethodRequest,
|
|
77
|
+
is_active: Callable[[], bool] | None = None,
|
|
75
78
|
) -> pb.InvokeExecuteMethodResponse:
|
|
76
79
|
request = await self._async_hydrator.execute_request(original)
|
|
77
80
|
flow = self._registry._flow_by_type(request.flow_type)
|
|
81
|
+
if request.step_type == _TIMEOUT_HANDLER_STEP_TYPE:
|
|
82
|
+
return await self._invoke_timeout_handler_async(request, flow)
|
|
78
83
|
step = flow.step(request.step_type)
|
|
79
84
|
condition_results = (
|
|
80
85
|
request.condition_results if request.HasField("condition_results") else None
|
|
@@ -87,6 +92,7 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
87
92
|
request.attributes,
|
|
88
93
|
request.step_exe_locals,
|
|
89
94
|
condition_results,
|
|
95
|
+
is_active=is_active,
|
|
90
96
|
)
|
|
91
97
|
input = self._values.decode(request.step_input, step.input_codec)
|
|
92
98
|
decision: Any = step.step.execute(context, input)
|
|
@@ -107,9 +113,56 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
107
113
|
flow.name, step.name, "execute", str(error)
|
|
108
114
|
) from error
|
|
109
115
|
|
|
116
|
+
async def _invoke_timeout_handler_async(
|
|
117
|
+
self,
|
|
118
|
+
request: pb.InvokeExecuteMethodRequest,
|
|
119
|
+
flow: Any,
|
|
120
|
+
) -> pb.InvokeExecuteMethodResponse:
|
|
121
|
+
if request.HasField("step_input"):
|
|
122
|
+
raise InvalidStepResultError(
|
|
123
|
+
flow.name, _TIMEOUT_HANDLER_STEP_TYPE, "execute", "input must be absent"
|
|
124
|
+
)
|
|
125
|
+
if not flow.has_timeout_handler:
|
|
126
|
+
raise InvalidStepResultError(
|
|
127
|
+
flow.name,
|
|
128
|
+
_TIMEOUT_HANDLER_STEP_TYPE,
|
|
129
|
+
"execute",
|
|
130
|
+
"handler is not registered",
|
|
131
|
+
)
|
|
132
|
+
condition_results = (
|
|
133
|
+
request.condition_results if request.HasField("condition_results") else None
|
|
134
|
+
)
|
|
135
|
+
context = InvocationContext(
|
|
136
|
+
InvocationMethod.EXECUTE,
|
|
137
|
+
flow,
|
|
138
|
+
request.context,
|
|
139
|
+
self._values,
|
|
140
|
+
request.attributes,
|
|
141
|
+
request.step_exe_locals,
|
|
142
|
+
condition_results,
|
|
143
|
+
)
|
|
144
|
+
decision: Any = flow.flow.handle_timeout(context)
|
|
145
|
+
if isawaitable(decision):
|
|
146
|
+
decision = await decision
|
|
147
|
+
try:
|
|
148
|
+
if not isinstance(decision, StepDecision):
|
|
149
|
+
raise TypeError("handle_timeout must return StepDecision")
|
|
150
|
+
return pb.InvokeExecuteMethodResponse(
|
|
151
|
+
step_decision=self._map_decision(flow, decision),
|
|
152
|
+
upsert_attributes=list(context.attribute_writes.values()),
|
|
153
|
+
record_events=context.events,
|
|
154
|
+
upsert_step_exe_locals=list(context.local_writes.values()),
|
|
155
|
+
publish_to_channel=context.publications,
|
|
156
|
+
)
|
|
157
|
+
except (TypeError, ValueError) as error:
|
|
158
|
+
raise InvalidStepResultError(
|
|
159
|
+
flow.name, _TIMEOUT_HANDLER_STEP_TYPE, "execute", str(error)
|
|
160
|
+
) from error
|
|
161
|
+
|
|
110
162
|
async def invoke_rpc( # type: ignore[override]
|
|
111
163
|
self,
|
|
112
164
|
original: pb.InvokeWorkerRPCRequest,
|
|
165
|
+
is_active: Callable[[], bool] | None = None,
|
|
113
166
|
) -> pb.InvokeWorkerRPCResponse:
|
|
114
167
|
request = await self._async_hydrator.rpc_request(original)
|
|
115
168
|
flow = self._registry._flow_by_type(request.flow_type)
|
|
@@ -121,6 +174,7 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
121
174
|
self._values,
|
|
122
175
|
request.attributes,
|
|
123
176
|
channel_infos=dict(request.channel_infos),
|
|
177
|
+
is_active=is_active,
|
|
124
178
|
)
|
|
125
179
|
arguments: list[object] = [context]
|
|
126
180
|
if rpc.input_codec is not None:
|
|
@@ -144,6 +198,11 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
144
198
|
response.step_decision.next_steps.extend(
|
|
145
199
|
self._map_movements(flow, returned.next_steps)
|
|
146
200
|
)
|
|
201
|
+
response.step_decision.cancel_step_types.extend(
|
|
202
|
+
self._map_cancellation_steps(flow, returned.canceling_steps)
|
|
203
|
+
)
|
|
204
|
+
if not returned.next_steps and not returned.canceling_steps:
|
|
205
|
+
response.ClearField("step_decision")
|
|
147
206
|
elif returned is None and rpc.output_codec is None:
|
|
148
207
|
response.output.CopyFrom(self._values.encode_dynamic(None))
|
|
149
208
|
else:
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import logging
|
|
12
|
+
from asyncio import CancelledError, current_task
|
|
12
13
|
from collections.abc import Awaitable, Callable
|
|
13
|
-
from typing import TypeVar
|
|
14
|
+
from typing import Any, TypeVar
|
|
14
15
|
|
|
15
16
|
import grpc
|
|
16
17
|
|
|
@@ -27,39 +28,71 @@ class AsyncWorkerService(dex_pb2_grpc.WorkerServiceServicer):
|
|
|
27
28
|
def __init__(self, dispatcher: AsyncWorkerDispatcher) -> None:
|
|
28
29
|
self._dispatcher = dispatcher
|
|
29
30
|
|
|
30
|
-
async def InvokeWaitForMethod(
|
|
31
|
+
async def InvokeWaitForMethod(
|
|
31
32
|
self,
|
|
32
33
|
request: pb.InvokeWaitForMethodRequest,
|
|
33
|
-
context: grpc.aio.ServicerContext
|
|
34
|
+
context: grpc.aio.ServicerContext[
|
|
35
|
+
pb.InvokeWaitForMethodRequest,
|
|
36
|
+
pb.InvokeWaitForMethodResponse,
|
|
37
|
+
],
|
|
34
38
|
) -> pb.InvokeWaitForMethodResponse:
|
|
35
39
|
return await self._invoke(
|
|
36
|
-
context,
|
|
40
|
+
context,
|
|
41
|
+
lambda: self._dispatcher.invoke_wait_for(
|
|
42
|
+
request, lambda: self._is_active(context)
|
|
43
|
+
),
|
|
37
44
|
)
|
|
38
45
|
|
|
39
|
-
async def InvokeExecuteMethod(
|
|
46
|
+
async def InvokeExecuteMethod(
|
|
40
47
|
self,
|
|
41
48
|
request: pb.InvokeExecuteMethodRequest,
|
|
42
|
-
context: grpc.aio.ServicerContext
|
|
49
|
+
context: grpc.aio.ServicerContext[
|
|
50
|
+
pb.InvokeExecuteMethodRequest,
|
|
51
|
+
pb.InvokeExecuteMethodResponse,
|
|
52
|
+
],
|
|
43
53
|
) -> pb.InvokeExecuteMethodResponse:
|
|
44
54
|
return await self._invoke(
|
|
45
|
-
context,
|
|
55
|
+
context,
|
|
56
|
+
lambda: self._dispatcher.invoke_execute(
|
|
57
|
+
request, lambda: self._is_active(context)
|
|
58
|
+
),
|
|
46
59
|
)
|
|
47
60
|
|
|
48
|
-
async def InvokeWorkerRPC(
|
|
61
|
+
async def InvokeWorkerRPC(
|
|
49
62
|
self,
|
|
50
63
|
request: pb.InvokeWorkerRPCRequest,
|
|
51
|
-
context: grpc.aio.ServicerContext
|
|
64
|
+
context: grpc.aio.ServicerContext[
|
|
65
|
+
pb.InvokeWorkerRPCRequest,
|
|
66
|
+
pb.InvokeWorkerRPCResponse,
|
|
67
|
+
],
|
|
52
68
|
) -> pb.InvokeWorkerRPCResponse:
|
|
53
|
-
return await self._invoke(
|
|
69
|
+
return await self._invoke(
|
|
70
|
+
context,
|
|
71
|
+
lambda: self._dispatcher.invoke_rpc(
|
|
72
|
+
request, lambda: self._is_active(context)
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def _is_active(context: grpc.aio.ServicerContext[Any, Any]) -> bool:
|
|
78
|
+
task = current_task()
|
|
79
|
+
return not context.cancelled() and (task is None or task.cancelling() == 0)
|
|
54
80
|
|
|
55
81
|
@staticmethod
|
|
56
82
|
async def _invoke(
|
|
57
|
-
context: grpc.aio.ServicerContext,
|
|
83
|
+
context: grpc.aio.ServicerContext[Any, Any],
|
|
58
84
|
invocation: Callable[[], Awaitable[ResponseT]],
|
|
59
85
|
) -> ResponseT:
|
|
60
86
|
try:
|
|
61
87
|
return await invocation()
|
|
88
|
+
except CancelledError:
|
|
89
|
+
raise
|
|
62
90
|
except BaseException as error:
|
|
91
|
+
if not AsyncWorkerService._is_active(context):
|
|
92
|
+
await context.abort(
|
|
93
|
+
grpc.StatusCode.CANCELLED,
|
|
94
|
+
"Python AsyncWorker invocation canceled",
|
|
95
|
+
)
|
|
63
96
|
_LOGGER.exception("Python AsyncWorker invocation failed")
|
|
64
97
|
await async_abort_worker_error(context, error)
|
|
65
98
|
raise RuntimeError("gRPC abort returned unexpectedly") from error
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
from enum import Enum
|
|
12
|
-
from typing import Any, Sequence, TypeVar, cast
|
|
12
|
+
from typing import Any, Callable, Sequence, TypeVar, cast
|
|
13
13
|
from urllib.parse import unquote
|
|
14
14
|
|
|
15
15
|
from dex._utils import require_name
|
|
@@ -19,6 +19,7 @@ from dex.channel import Channel, ChannelMap
|
|
|
19
19
|
from dex.codec import Codec
|
|
20
20
|
from dex.dexpb import dex_pb2 as pb
|
|
21
21
|
from dex.flow import Registry, _RegisteredFlow
|
|
22
|
+
from dex.flow_result import FlowResult, flow_result_from_proto
|
|
22
23
|
|
|
23
24
|
ValueT = TypeVar("ValueT")
|
|
24
25
|
_Definition = Attribute[Any] | AttributeMap[Any] | Channel[Any] | ChannelMap[Any]
|
|
@@ -41,6 +42,7 @@ class InvocationContext:
|
|
|
41
42
|
locals: Sequence[pb.KV] = (),
|
|
42
43
|
condition_results: pb.ConditionResults | None = None,
|
|
43
44
|
channel_infos: dict[str, pb.ChannelInfo] | None = None,
|
|
45
|
+
is_active: Callable[[], bool] | None = None,
|
|
44
46
|
) -> None:
|
|
45
47
|
self._method = method
|
|
46
48
|
self._flow = flow
|
|
@@ -50,6 +52,7 @@ class InvocationContext:
|
|
|
50
52
|
self._locals = self._map_values("step-execution local", locals)
|
|
51
53
|
self._condition_results = condition_results
|
|
52
54
|
self._channel_infos = dict(channel_infos or {})
|
|
55
|
+
self._is_active = is_active or _always_active
|
|
53
56
|
self.attribute_writes: dict[str, pb.AttributeWrite] = {}
|
|
54
57
|
self.local_writes: dict[str, pb.KV] = {}
|
|
55
58
|
self.events: list[pb.KV] = []
|
|
@@ -96,6 +99,29 @@ class InvocationContext:
|
|
|
96
99
|
and self._condition_results.wait_for_failed
|
|
97
100
|
)
|
|
98
101
|
|
|
102
|
+
def sub_flow_result(self, index: int = 0) -> FlowResult:
|
|
103
|
+
if self._method is not InvocationMethod.EXECUTE:
|
|
104
|
+
raise ValueError("SubFlow results are available only during execute")
|
|
105
|
+
if (
|
|
106
|
+
self._condition_results is None
|
|
107
|
+
or index < 0
|
|
108
|
+
or index >= len(self._condition_results.sub_flow_results)
|
|
109
|
+
):
|
|
110
|
+
raise ValueError(f"SubFlow result index is unavailable: {index}")
|
|
111
|
+
return flow_result_from_proto(
|
|
112
|
+
self._condition_results.sub_flow_results[index],
|
|
113
|
+
lambda value, output_type: self._values.decode(
|
|
114
|
+
value, self._values.codec(output_type)
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def sub_flow_id(self, index: int = 0) -> str:
|
|
119
|
+
self.sub_flow_result(index)
|
|
120
|
+
return f"SubFlow:{self.flow_id}-{self.step_execution_id}-{index}"
|
|
121
|
+
|
|
122
|
+
def is_cancellation_requested(self) -> bool:
|
|
123
|
+
return not self._is_active()
|
|
124
|
+
|
|
99
125
|
def set_step_execution_local(self, key: str, value: object) -> None:
|
|
100
126
|
require_name(key)
|
|
101
127
|
self.local_writes[key] = pb.KV(
|
|
@@ -302,3 +328,7 @@ class InvocationContext:
|
|
|
302
328
|
if value_type is float:
|
|
303
329
|
return 0.0
|
|
304
330
|
return None
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _always_active() -> bool:
|
|
334
|
+
return True
|
|
@@ -88,13 +88,19 @@ class ValueHydrator:
|
|
|
88
88
|
) -> pb.InvokeExecuteMethodRequest:
|
|
89
89
|
result = pb.InvokeExecuteMethodRequest()
|
|
90
90
|
result.CopyFrom(request)
|
|
91
|
-
|
|
91
|
+
has_step_input = request.HasField("step_input")
|
|
92
|
+
values = [request.step_input] if has_step_input else []
|
|
92
93
|
values.extend(entry.value for entry in request.attributes)
|
|
93
94
|
values.extend(entry.value for entry in request.step_exe_locals)
|
|
94
95
|
for channel_result in request.condition_results.channel_results:
|
|
95
96
|
values.extend(channel_result.values)
|
|
97
|
+
for flow_result in request.condition_results.sub_flow_results:
|
|
98
|
+
values.extend(
|
|
99
|
+
completion.completed_step_output for completion in flow_result.results
|
|
100
|
+
)
|
|
96
101
|
hydrated = iter(self.hydrate_all(values))
|
|
97
|
-
|
|
102
|
+
if has_step_input:
|
|
103
|
+
result.step_input.CopyFrom(next(hydrated))
|
|
98
104
|
for entry in result.attributes:
|
|
99
105
|
entry.value.CopyFrom(next(hydrated))
|
|
100
106
|
for entry in result.step_exe_locals:
|
|
@@ -102,6 +108,9 @@ class ValueHydrator:
|
|
|
102
108
|
for channel_result in result.condition_results.channel_results:
|
|
103
109
|
for value in channel_result.values:
|
|
104
110
|
value.CopyFrom(next(hydrated))
|
|
111
|
+
for flow_result in result.condition_results.sub_flow_results:
|
|
112
|
+
for completion in flow_result.results:
|
|
113
|
+
completion.completed_step_output.CopyFrom(next(hydrated))
|
|
105
114
|
return result
|
|
106
115
|
|
|
107
116
|
def rpc_request(
|