dex-python-sdk 0.2.11__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/PKG-INFO +24 -1
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/README.md +23 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/__init__.py +4 -1
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/_async_value_hydrator.py +28 -2
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/_async_worker_dispatcher.py +61 -8
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/_invocation_context.py +35 -24
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/_value_hydrator.py +28 -2
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/_worker_dispatcher.py +205 -6
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/async_client.py +26 -1
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/client.py +26 -1
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/context.py +23 -0
- dex_python_sdk-0.3.2/dex/dexpb/dex_pb2.py +495 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/dexpb/dex_pb2.pyi +124 -22
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/flow.py +35 -4
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/flow_options.py +69 -3
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/step.py +26 -2
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/pyproject.toml +1 -1
- dex_python_sdk-0.2.11/dex/dexpb/dex_pb2.py +0 -477
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/LEGACY_NOTICES.md +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/LICENSE +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/_async_worker_service.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/_grpc_errors.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/_native.pyi +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/_utils.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/_value_mapper.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/_worker_service.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/async_worker.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/attribute.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/blob_cache.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/channel.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/client_options.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/codec.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/condition.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/dexpb/__init__.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/dexpb/dex_pb2_grpc.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/flow_config.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/flow_info.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/flow_result.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/py.typed +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/runtime_errors.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/step_execution.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/stream.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/subflow.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/timer.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/wait.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/worker.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/dex/worker_options.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/sdk-rust/Cargo.lock +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/sdk-rust/Cargo.toml +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/sdk-rust/crates/dex-blob-cache/Cargo.toml +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/sdk-rust/crates/dex-blob-cache/LICENSE +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/sdk-rust/crates/dex-blob-cache/src/config.rs +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/sdk-rust/crates/dex-blob-cache/src/entry.rs +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/sdk-rust/crates/dex-blob-cache/src/error.rs +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/sdk-rust/crates/dex-blob-cache/src/format.rs +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/sdk-rust/crates/dex-blob-cache/src/lib.rs +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/sdk-rust/crates/dex-blob-cache/src/policy.rs +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/sdk-rust/crates/dex-blob-cache/src/store.rs +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/sdk-rust/crates/dex-blob-cache/tests/blob_cache_integration.rs +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/sdk-rust/crates/dex-blob-cache-python/Cargo.toml +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/sdk-rust/crates/dex-blob-cache-python/LICENSE +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.2}/sdk-rust/crates/dex-blob-cache-python/src/lib.rs +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dex-python-sdk
|
|
3
|
-
Version: 0.2
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Requires-Dist: grpcio>=1.83.0
|
|
5
5
|
Requires-Dist: grpcio-status>=1.83.0
|
|
6
6
|
Requires-Dist: protobuf>=7.35.1
|
|
@@ -59,6 +59,29 @@ atomic commit and Channel deletion validation. Attribute locks add isolation
|
|
|
59
59
|
only among cooperating Steps and RPCs using the same lock. Write-only publish
|
|
60
60
|
and delete operations do not require loading.
|
|
61
61
|
|
|
62
|
+
## Step and timeout-handler state loading
|
|
63
|
+
|
|
64
|
+
`StepOptions` provides the same five selections independently for `wait_for` and
|
|
65
|
+
`execute`: all AttributeMap instances, exact AttributeMap instances, Channels,
|
|
66
|
+
all ChannelMap instances, and exact ChannelMap instances. The methods receive
|
|
67
|
+
independent snapshots. Execute reads after the winning Wait consumes messages;
|
|
68
|
+
retries of one logical method call reuse its first snapshot.
|
|
69
|
+
|
|
70
|
+
`FlowTimeoutHandlerOptions` provides Execute-style timeouts, heartbeat detection,
|
|
71
|
+
retry, durability, Attribute locks, and the same state selections. Set it on
|
|
72
|
+
`StartFlowOptions` or `SubFlowOptions` only for a positive timeout using `HANDLER`.
|
|
73
|
+
Exhausted retries may proceed to a registered `Step[None]`; read the final
|
|
74
|
+
failure from `Context.recovery_error`.
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
class TimeoutRecoveryStep(Step[None]):
|
|
78
|
+
def execute(self, context: Context, input: None) -> StepDecision:
|
|
79
|
+
failure = context.recovery_error
|
|
80
|
+
if failure is None:
|
|
81
|
+
return force_fail("timeout recovery has no failure")
|
|
82
|
+
return force_fail(failure.detail)
|
|
83
|
+
```
|
|
84
|
+
|
|
62
85
|
Python SDK for [Dex workflow engine](https://github.com/superdurable/dex)
|
|
63
86
|
|
|
64
87
|
## New user contracts
|
|
@@ -43,6 +43,29 @@ atomic commit and Channel deletion validation. Attribute locks add isolation
|
|
|
43
43
|
only among cooperating Steps and RPCs using the same lock. Write-only publish
|
|
44
44
|
and delete operations do not require loading.
|
|
45
45
|
|
|
46
|
+
## Step and timeout-handler state loading
|
|
47
|
+
|
|
48
|
+
`StepOptions` provides the same five selections independently for `wait_for` and
|
|
49
|
+
`execute`: all AttributeMap instances, exact AttributeMap instances, Channels,
|
|
50
|
+
all ChannelMap instances, and exact ChannelMap instances. The methods receive
|
|
51
|
+
independent snapshots. Execute reads after the winning Wait consumes messages;
|
|
52
|
+
retries of one logical method call reuse its first snapshot.
|
|
53
|
+
|
|
54
|
+
`FlowTimeoutHandlerOptions` provides Execute-style timeouts, heartbeat detection,
|
|
55
|
+
retry, durability, Attribute locks, and the same state selections. Set it on
|
|
56
|
+
`StartFlowOptions` or `SubFlowOptions` only for a positive timeout using `HANDLER`.
|
|
57
|
+
Exhausted retries may proceed to a registered `Step[None]`; read the final
|
|
58
|
+
failure from `Context.recovery_error`.
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
class TimeoutRecoveryStep(Step[None]):
|
|
62
|
+
def execute(self, context: Context, input: None) -> StepDecision:
|
|
63
|
+
failure = context.recovery_error
|
|
64
|
+
if failure is None:
|
|
65
|
+
return force_fail("timeout recovery has no failure")
|
|
66
|
+
return force_fail(failure.detail)
|
|
67
|
+
```
|
|
68
|
+
|
|
46
69
|
Python SDK for [Dex workflow engine](https://github.com/superdurable/dex)
|
|
47
70
|
|
|
48
71
|
## New user contracts
|
|
@@ -36,7 +36,7 @@ from dex.codec import (
|
|
|
36
36
|
WireKind,
|
|
37
37
|
)
|
|
38
38
|
from dex.condition import ConditionCombination
|
|
39
|
-
from dex.context import AsyncContext, Context
|
|
39
|
+
from dex.context import AsyncContext, Context, RecoveryErrorInfo
|
|
40
40
|
from dex.flow import Flow, PersistenceSchema, Registry, RPCResult, rpc
|
|
41
41
|
from dex.flow_config import ActiveStepSearchMode, FlowConfig
|
|
42
42
|
from dex.flow_info import (
|
|
@@ -47,6 +47,7 @@ from dex.flow_info import (
|
|
|
47
47
|
SearchFlowsPage,
|
|
48
48
|
)
|
|
49
49
|
from dex.flow_options import (
|
|
50
|
+
FlowTimeoutHandlerOptions,
|
|
50
51
|
FlowTimeoutPolicy,
|
|
51
52
|
IdReusePolicy,
|
|
52
53
|
StartFlowOptions,
|
|
@@ -152,6 +153,7 @@ __all__ = [
|
|
|
152
153
|
"FlowNotActiveError",
|
|
153
154
|
"FlowNotFoundError",
|
|
154
155
|
"FlowStatus",
|
|
156
|
+
"FlowTimeoutHandlerOptions",
|
|
155
157
|
"FlowTimeoutPolicy",
|
|
156
158
|
"HealthInfo",
|
|
157
159
|
"IdReusePolicy",
|
|
@@ -161,6 +163,7 @@ __all__ = [
|
|
|
161
163
|
"LongPollTimeoutError",
|
|
162
164
|
"PersistenceSchema",
|
|
163
165
|
"RPCResult",
|
|
166
|
+
"RecoveryErrorInfo",
|
|
164
167
|
"RetryAfterError",
|
|
165
168
|
"RpcLockConflictError",
|
|
166
169
|
"Registry",
|
|
@@ -80,12 +80,25 @@ class AsyncValueHydrator:
|
|
|
80
80
|
if has_heartbeat:
|
|
81
81
|
values.append(request.context.last_heartbeat_value)
|
|
82
82
|
values.extend(entry.value for entry in request.attributes)
|
|
83
|
+
channel_messages = [
|
|
84
|
+
message
|
|
85
|
+
for channel_values in request.loaded_channel_messages.values()
|
|
86
|
+
for message in channel_values.messages
|
|
87
|
+
]
|
|
88
|
+
values.extend(message.value for message in channel_messages)
|
|
83
89
|
hydrated = iter(await self.hydrate_all(values))
|
|
84
90
|
result.step_input.CopyFrom(next(hydrated))
|
|
85
91
|
if has_heartbeat:
|
|
86
92
|
result.context.last_heartbeat_value.CopyFrom(next(hydrated))
|
|
87
|
-
for entry
|
|
88
|
-
entry.value.CopyFrom(
|
|
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
|
-
|
|
157
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
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
|
-
|
|
321
|
+
failure = InvalidStepResultError(
|
|
279
322
|
flow.name, _TIMEOUT_HANDLER_STEP_TYPE, "execute", str(error)
|
|
280
|
-
)
|
|
323
|
+
)
|
|
324
|
+
cause = error
|
|
325
|
+
except BaseException as error:
|
|
326
|
+
failure = error
|
|
327
|
+
combined = context._finalize_step_outputs(failure)
|
|
328
|
+
if combined is not None:
|
|
329
|
+
if cause is not None:
|
|
330
|
+
raise combined from cause
|
|
331
|
+
raise combined
|
|
332
|
+
assert response is not None
|
|
333
|
+
return response
|
|
281
334
|
|
|
282
335
|
async def invoke_rpc( # type: ignore[override]
|
|
283
336
|
self,
|
|
@@ -17,6 +17,7 @@ from dex._value_mapper import ValueMapper
|
|
|
17
17
|
from dex.attribute import Attribute, AttributeMap, _apply_attribute_store_sync
|
|
18
18
|
from dex.channel import Channel, ChannelMap, ChannelMessage
|
|
19
19
|
from dex.codec import Codec
|
|
20
|
+
from dex.context import RecoveryErrorInfo
|
|
20
21
|
from dex.dexpb import dex_pb2 as pb
|
|
21
22
|
from dex.flow import Registry, _RegisteredFlow
|
|
22
23
|
from dex.flow_result import FlowResult, flow_result_from_proto
|
|
@@ -116,6 +117,15 @@ class InvocationContext:
|
|
|
116
117
|
def from_step_execution_id(self) -> str:
|
|
117
118
|
return self._metadata.from_step_execution_id
|
|
118
119
|
|
|
120
|
+
@property
|
|
121
|
+
def recovery_error(self) -> RecoveryErrorInfo | None:
|
|
122
|
+
if not self._metadata.HasField("recovery_error"):
|
|
123
|
+
return None
|
|
124
|
+
return RecoveryErrorInfo(
|
|
125
|
+
detail=self._metadata.recovery_error.detail,
|
|
126
|
+
error_type=self._metadata.recovery_error.error_type,
|
|
127
|
+
)
|
|
128
|
+
|
|
119
129
|
@property
|
|
120
130
|
def attempt(self) -> int:
|
|
121
131
|
return self._metadata.attempt
|
|
@@ -139,7 +149,12 @@ class InvocationContext:
|
|
|
139
149
|
|
|
140
150
|
async def heartbeat(self, value: object = _NO_HEARTBEAT_VALUE) -> None:
|
|
141
151
|
if (
|
|
142
|
-
self._method
|
|
152
|
+
self._method
|
|
153
|
+
not in (
|
|
154
|
+
InvocationMethod.WAIT_FOR,
|
|
155
|
+
InvocationMethod.EXECUTE,
|
|
156
|
+
InvocationMethod.TIMEOUT,
|
|
157
|
+
)
|
|
143
158
|
or self._output_emitter is None
|
|
144
159
|
):
|
|
145
160
|
raise ValueError("heartbeat requires an asynchronous Step Context")
|
|
@@ -226,7 +241,11 @@ class InvocationContext:
|
|
|
226
241
|
definition: Stream[ValueT],
|
|
227
242
|
value: ValueT,
|
|
228
243
|
) -> StepOutput | None:
|
|
229
|
-
if self._method not in (
|
|
244
|
+
if self._method not in (
|
|
245
|
+
InvocationMethod.WAIT_FOR,
|
|
246
|
+
InvocationMethod.EXECUTE,
|
|
247
|
+
InvocationMethod.TIMEOUT,
|
|
248
|
+
):
|
|
230
249
|
raise ValueError("Stream writes require a Step Context")
|
|
231
250
|
self._require_registered(definition)
|
|
232
251
|
output = _StreamStepOutput(
|
|
@@ -245,7 +264,11 @@ class InvocationContext:
|
|
|
245
264
|
return None
|
|
246
265
|
|
|
247
266
|
def _prepare_buffered_stream(self, definition: Stream[object]) -> bool:
|
|
248
|
-
if self._method not in (
|
|
267
|
+
if self._method not in (
|
|
268
|
+
InvocationMethod.WAIT_FOR,
|
|
269
|
+
InvocationMethod.EXECUTE,
|
|
270
|
+
InvocationMethod.TIMEOUT,
|
|
271
|
+
):
|
|
249
272
|
raise ValueError("Buffered Streams require a Step Context")
|
|
250
273
|
self._require_registered(definition)
|
|
251
274
|
return self._output_emitter is not None
|
|
@@ -357,11 +380,10 @@ class InvocationContext:
|
|
|
357
380
|
),
|
|
358
381
|
)
|
|
359
382
|
)
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
)
|
|
383
|
+
current = self._channel_infos.get(name)
|
|
384
|
+
self._channel_infos[name] = pb.ChannelInfo(
|
|
385
|
+
size=(current.size if current is not None else 0) + 1
|
|
386
|
+
)
|
|
365
387
|
|
|
366
388
|
def _delete_channel_message(
|
|
367
389
|
self,
|
|
@@ -369,8 +391,6 @@ class InvocationContext:
|
|
|
369
391
|
instance: str | None,
|
|
370
392
|
message_id: str,
|
|
371
393
|
) -> None:
|
|
372
|
-
if self._method is not InvocationMethod.RPC:
|
|
373
|
-
raise ValueError("Channel message deletion requires an RPC Context")
|
|
374
394
|
self._require_registered(definition)
|
|
375
395
|
name = self._physical_name(definition, instance)
|
|
376
396
|
self.channel_deletions.append(
|
|
@@ -410,8 +430,6 @@ class InvocationContext:
|
|
|
410
430
|
definition: ChannelMap[object],
|
|
411
431
|
) -> tuple[str, ...]:
|
|
412
432
|
self._require_registered(definition)
|
|
413
|
-
if self._method is not InvocationMethod.RPC:
|
|
414
|
-
raise ValueError("ChannelMap introspection requires an RPC invocation")
|
|
415
433
|
prefix = f"{definition.name}/"
|
|
416
434
|
return tuple(
|
|
417
435
|
sorted(
|
|
@@ -435,8 +453,6 @@ class InvocationContext:
|
|
|
435
453
|
definition: Channel[ValueT] | ChannelMap[ValueT],
|
|
436
454
|
instance: str | None,
|
|
437
455
|
) -> tuple[ChannelMessage[ValueT], ...]:
|
|
438
|
-
if self._method is not InvocationMethod.RPC:
|
|
439
|
-
raise ValueError("pending Channel messages require an RPC Context")
|
|
440
456
|
self._require_registered(definition)
|
|
441
457
|
if isinstance(definition, ChannelMap):
|
|
442
458
|
channel_name = self._physical_name(definition, instance)
|
|
@@ -449,7 +465,7 @@ class InvocationContext:
|
|
|
449
465
|
is_loaded = definition.name in self._loaded_channel_names
|
|
450
466
|
if not is_loaded:
|
|
451
467
|
raise ChannelMessagesNotLoadedError(
|
|
452
|
-
f"Channel messages were not loaded for
|
|
468
|
+
f"Channel messages were not loaded for this invocation: {definition.name}"
|
|
453
469
|
)
|
|
454
470
|
values = self._loaded_channel_messages.get(channel_name)
|
|
455
471
|
if values is None:
|
|
@@ -489,9 +505,7 @@ class InvocationContext:
|
|
|
489
505
|
definition: Attribute[Any] | AttributeMap[Any],
|
|
490
506
|
instance: str | None,
|
|
491
507
|
) -> None:
|
|
492
|
-
if
|
|
493
|
-
definition, AttributeMap
|
|
494
|
-
):
|
|
508
|
+
if not isinstance(definition, AttributeMap):
|
|
495
509
|
return
|
|
496
510
|
physical_name = self._physical_name(definition, instance)
|
|
497
511
|
if (
|
|
@@ -499,19 +513,16 @@ class InvocationContext:
|
|
|
499
513
|
and physical_name not in self._loaded_attribute_map_instances
|
|
500
514
|
):
|
|
501
515
|
raise AttributeMapNotLoadedError(
|
|
502
|
-
f"AttributeMap instance was not loaded for
|
|
516
|
+
f"AttributeMap instance was not loaded for this invocation: {physical_name}"
|
|
503
517
|
)
|
|
504
518
|
|
|
505
519
|
def _require_attribute_map_all_loaded(
|
|
506
520
|
self,
|
|
507
521
|
definition: AttributeMap[object],
|
|
508
522
|
) -> None:
|
|
509
|
-
if
|
|
510
|
-
self._method is InvocationMethod.RPC
|
|
511
|
-
and f"{definition.name}/" not in self._loaded_attribute_map_instances
|
|
512
|
-
):
|
|
523
|
+
if f"{definition.name}/" not in self._loaded_attribute_map_instances:
|
|
513
524
|
raise AttributeMapNotLoadedError(
|
|
514
|
-
f"all AttributeMap instances were not loaded for
|
|
525
|
+
f"all AttributeMap instances were not loaded for this invocation: {definition.name}"
|
|
515
526
|
)
|
|
516
527
|
|
|
517
528
|
def _require_registered(self, definition: _Definition) -> None:
|
|
@@ -80,12 +80,25 @@ class ValueHydrator:
|
|
|
80
80
|
if has_heartbeat:
|
|
81
81
|
values.append(request.context.last_heartbeat_value)
|
|
82
82
|
values.extend(entry.value for entry in request.attributes)
|
|
83
|
+
channel_messages = [
|
|
84
|
+
message
|
|
85
|
+
for channel_values in request.loaded_channel_messages.values()
|
|
86
|
+
for message in channel_values.messages
|
|
87
|
+
]
|
|
88
|
+
values.extend(message.value for message in channel_messages)
|
|
83
89
|
hydrated = iter(self.hydrate_all(values))
|
|
84
90
|
result.step_input.CopyFrom(next(hydrated))
|
|
85
91
|
if has_heartbeat:
|
|
86
92
|
result.context.last_heartbeat_value.CopyFrom(next(hydrated))
|
|
87
|
-
for entry
|
|
88
|
-
entry.value.CopyFrom(
|
|
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))
|