dex-python-sdk 0.0.2__cp311-abi3-win_amd64.whl
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/__init__.py +146 -0
- dex/_grpc_errors.py +73 -0
- dex/_invocation_context.py +269 -0
- dex/_native.pyd +0 -0
- dex/_native.pyi +20 -0
- dex/_utils.py +18 -0
- dex/_value_hydrator.py +223 -0
- dex/_value_mapper.py +174 -0
- dex/_worker_dispatcher.py +451 -0
- dex/_worker_service.py +60 -0
- dex/attribute.py +87 -0
- dex/blob_cache.py +73 -0
- dex/channel.py +165 -0
- dex/client.py +715 -0
- dex/client_options.py +19 -0
- dex/codec.py +286 -0
- dex/command_request.py +120 -0
- dex/command_results.py +107 -0
- dex/communication.py +136 -0
- dex/communication_schema.py +54 -0
- dex/condition.py +74 -0
- dex/context.py +86 -0
- dex/data_attributes.py +70 -0
- dex/dexpb/__init__.py +1 -0
- dex/dexpb/dex_pb2.py +381 -0
- dex/dexpb/dex_pb2.pyi +1734 -0
- dex/dexpb/dex_pb2_grpc.py +1298 -0
- dex/errors.py +109 -0
- dex/flow.py +456 -0
- dex/flow_config.py +29 -0
- dex/flow_info.py +51 -0
- dex/flow_options.py +122 -0
- dex/object_encoder.py +799 -0
- dex/persistence.py +89 -0
- dex/persistence_options.py +12 -0
- dex/persistence_schema.py +51 -0
- dex/py.typed +1 -0
- dex/registry.py +204 -0
- dex/reset_workflow_type_and_options.py +67 -0
- dex/rpc.py +93 -0
- dex/runtime_errors.py +81 -0
- dex/search_attributes.py +184 -0
- dex/state_decision.py +153 -0
- dex/state_execution_locals.py +66 -0
- dex/state_movement.py +115 -0
- dex/state_schema.py +48 -0
- dex/step.py +194 -0
- dex/step_execution.py +42 -0
- dex/stop_workflow_options.py +18 -0
- dex/tests/__init__.py +80 -0
- dex/tests/dex-service-env/.env +7 -0
- dex/tests/dex-service-env/docker-compose-init.sh +44 -0
- dex/tests/dex-service-env/docker-compose.yml +97 -0
- dex/tests/dex-service-env/dynamicconfig/README.md +39 -0
- dex/tests/dex-service-env/dynamicconfig/development-sql.yaml +9 -0
- dex/tests/dex-service-env/dynamicconfig/docker.yaml +2 -0
- dex/tests/test_abnormal_exit_workflow.py +43 -0
- dex/tests/test_basic_workflow.py +70 -0
- dex/tests/test_conditional_complete.py +50 -0
- dex/tests/test_describe_workflow.py +40 -0
- dex/tests/test_empty_data_decodes_properly.py +74 -0
- dex/tests/test_internal_channel.py +28 -0
- dex/tests/test_internal_channel_with_no_prefix_channel.py +41 -0
- dex/tests/test_persistence_data_attributes.py +62 -0
- dex/tests/test_persistence_search_attributes.py +127 -0
- dex/tests/test_persistence_state_execution_locals.py +38 -0
- dex/tests/test_rpc.py +64 -0
- dex/tests/test_rpc_with_memo.py +195 -0
- dex/tests/test_rpc_with_memo_duplicate_java_tests.py +117 -0
- dex/tests/test_signal.py +51 -0
- dex/tests/test_skip_wait_until.py +76 -0
- dex/tests/test_state_failure_recovery.py +28 -0
- dex/tests/test_timer.py +35 -0
- dex/tests/test_wait_for_state_execution_completion.py +53 -0
- dex/tests/test_workflow_errors.py +87 -0
- dex/tests/test_workflow_state_options.py +118 -0
- dex/tests/test_workflow_state_options_override.py +44 -0
- dex/tests/worker_server.py +64 -0
- dex/tests/workflows/abnormal_exit_workflow.py +42 -0
- dex/tests/workflows/basic_workflow.py +62 -0
- dex/tests/workflows/conditional_complete_workflow.py +95 -0
- dex/tests/workflows/describe_workflow.py +46 -0
- dex/tests/workflows/empty_data_workflow.py +45 -0
- dex/tests/workflows/internal_channel_workflow.py +129 -0
- dex/tests/workflows/internal_channel_workflow_with_no_prefix_channel.py +100 -0
- dex/tests/workflows/java_duplicate_rpc_memo_workflow.py +276 -0
- dex/tests/workflows/persistence_data_attributes_workflow.py +98 -0
- dex/tests/workflows/persistence_search_attributes_workflow.py +159 -0
- dex/tests/workflows/persistence_state_execution_local_workflow.py +63 -0
- dex/tests/workflows/recovery_workflow.py +82 -0
- dex/tests/workflows/rpc_memo_workflow.py +231 -0
- dex/tests/workflows/rpc_workflow.py +117 -0
- dex/tests/workflows/state_options_override_workflow.py +93 -0
- dex/tests/workflows/state_options_workflow.py +84 -0
- dex/tests/workflows/timer_workflow.py +46 -0
- dex/tests/workflows/wait_for_state_with_state_execution_id_workflow.py +70 -0
- dex/tests/workflows/wait_for_state_with_wait_for_key_workflow.py +71 -0
- dex/tests/workflows/wait_internal_channel_workflow.py +47 -0
- dex/tests/workflows/wait_signal_workflow.py +147 -0
- dex/timer.py +21 -0
- dex/type_store.py +99 -0
- dex/unregistered_client.py +585 -0
- dex/utils/__init__.py +3 -0
- dex/utils/dex_typing.py +25 -0
- dex/utils/persistence_utils.py +32 -0
- dex/wait.py +49 -0
- dex/worker.py +121 -0
- dex/worker_options.py +22 -0
- dex/worker_service.py +432 -0
- dex/workflow.py +79 -0
- dex/workflow_context.py +44 -0
- dex/workflow_info.py +16 -0
- dex/workflow_options.py +74 -0
- dex/workflow_state.py +123 -0
- dex/workflow_state_options.py +154 -0
- dex_python_sdk-0.0.2.dist-info/METADATA +202 -0
- dex_python_sdk-0.0.2.dist-info/RECORD +121 -0
- dex_python_sdk-0.0.2.dist-info/WHEEL +4 -0
- dex_python_sdk-0.0.2.dist-info/licenses/LEGACY_NOTICES.md +61 -0
- dex_python_sdk-0.0.2.dist-info/licenses/LICENSE +192 -0
- dex_python_sdk-0.0.2.dist-info/sboms/dex-blob-cache-python.cyclonedx.json +2406 -0
dex/client.py
ADDED
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
# Legacy Materials in this file remain under their original licenses.
|
|
2
|
+
# See LEGACY_NOTICES.md.
|
|
3
|
+
|
|
4
|
+
# Modifications Copyright (c) 2026 Super Durable, Inc.
|
|
5
|
+
#
|
|
6
|
+
# Modifications after the Legacy Cutoff are licensed under the
|
|
7
|
+
# Super Durable Source License 1.0.
|
|
8
|
+
# Legacy Materials remain under their original licenses.
|
|
9
|
+
# See LICENSE and LEGACY_NOTICES.md.
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from datetime import timedelta, timezone
|
|
14
|
+
from types import TracebackType
|
|
15
|
+
from typing import Any, Callable, TypeVar, cast, overload
|
|
16
|
+
from uuid import uuid4
|
|
17
|
+
|
|
18
|
+
import grpc
|
|
19
|
+
|
|
20
|
+
from dex._grpc_errors import translate_rpc_error
|
|
21
|
+
from dex._utils import require_name
|
|
22
|
+
from dex._value_hydrator import ValueHydrator
|
|
23
|
+
from dex._value_mapper import ValueMapper
|
|
24
|
+
from dex._worker_dispatcher import WorkerDispatcher
|
|
25
|
+
from dex.attribute import Attribute, AttributeMap
|
|
26
|
+
from dex.blob_cache import BlobCache
|
|
27
|
+
from dex.channel import Channel, ChannelMap
|
|
28
|
+
from dex.client_options import ClientOptions
|
|
29
|
+
from dex.context import Context
|
|
30
|
+
from dex.dexpb import dex_pb2 as pb
|
|
31
|
+
from dex.dexpb import dex_pb2_grpc
|
|
32
|
+
from dex.flow import Flow, Registry, RPCResult
|
|
33
|
+
from dex.flow_config import ActiveStepSearchMode, FlowConfig
|
|
34
|
+
from dex.flow_info import FlowInfo, FlowStatus
|
|
35
|
+
from dex.flow_options import (
|
|
36
|
+
IdReusePolicy,
|
|
37
|
+
ResetFlowOptions,
|
|
38
|
+
ResetType,
|
|
39
|
+
StartFlowOptions,
|
|
40
|
+
StopFlowOptions,
|
|
41
|
+
StopType,
|
|
42
|
+
)
|
|
43
|
+
from dex.runtime_errors import (
|
|
44
|
+
FlowErrorType,
|
|
45
|
+
FlowUncompletedError,
|
|
46
|
+
LongPollTimeoutError,
|
|
47
|
+
)
|
|
48
|
+
from dex.step import RetryPolicy, StepDurability
|
|
49
|
+
from dex.step_execution import StepExecutionId, TimerId
|
|
50
|
+
|
|
51
|
+
InputT = TypeVar("InputT")
|
|
52
|
+
OutputT = TypeVar("OutputT")
|
|
53
|
+
ValueT = TypeVar("ValueT")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Client:
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
registry: Registry,
|
|
60
|
+
blob_cache: BlobCache,
|
|
61
|
+
options: ClientOptions | None = None,
|
|
62
|
+
) -> None:
|
|
63
|
+
self.registry = registry
|
|
64
|
+
self.blob_cache = blob_cache
|
|
65
|
+
self.options = options or ClientOptions()
|
|
66
|
+
self._channel = grpc.insecure_channel(self.options.server_address)
|
|
67
|
+
self._service = dex_pb2_grpc.FlowServiceStub( # type: ignore[no-untyped-call]
|
|
68
|
+
self._channel
|
|
69
|
+
)
|
|
70
|
+
self._values = ValueMapper(registry.codec_registry)
|
|
71
|
+
self._hydrator = ValueHydrator(self._service, blob_cache)
|
|
72
|
+
self._mappings = WorkerDispatcher(
|
|
73
|
+
registry,
|
|
74
|
+
self._values,
|
|
75
|
+
self._hydrator,
|
|
76
|
+
)
|
|
77
|
+
self._closed = False
|
|
78
|
+
|
|
79
|
+
def __enter__(self) -> Client:
|
|
80
|
+
return self
|
|
81
|
+
|
|
82
|
+
def __exit__(
|
|
83
|
+
self,
|
|
84
|
+
exception_type: type[BaseException] | None,
|
|
85
|
+
exception: BaseException | None,
|
|
86
|
+
traceback: TracebackType | None,
|
|
87
|
+
) -> None:
|
|
88
|
+
self.close()
|
|
89
|
+
|
|
90
|
+
def start_flow(
|
|
91
|
+
self,
|
|
92
|
+
flow: Flow[InputT],
|
|
93
|
+
flow_id: str,
|
|
94
|
+
input: InputT,
|
|
95
|
+
options: StartFlowOptions = StartFlowOptions(),
|
|
96
|
+
) -> str:
|
|
97
|
+
registered = self.registry._flow_for_instance(flow)
|
|
98
|
+
request = pb.StartFlowRequest(
|
|
99
|
+
flow_id=require_name(flow_id),
|
|
100
|
+
flow_type=registered.name,
|
|
101
|
+
request_id=options.request_id or str(uuid4()),
|
|
102
|
+
flow_start_options=self._map_start_options(options),
|
|
103
|
+
)
|
|
104
|
+
if registered.start_step is not None:
|
|
105
|
+
start = registered.start_step
|
|
106
|
+
request.start_step_type = start.name
|
|
107
|
+
request.step_input.CopyFrom(self._values.encode(input, start.input_codec))
|
|
108
|
+
step_options = self._mappings.map_step_options(
|
|
109
|
+
registered,
|
|
110
|
+
start.step.get_step_options(),
|
|
111
|
+
)
|
|
112
|
+
if step_options is not None:
|
|
113
|
+
request.step_options.CopyFrom(step_options)
|
|
114
|
+
request.step_options.skip_wait_for = start.skips_wait_for
|
|
115
|
+
elif input is not None:
|
|
116
|
+
raise ValueError("Flow without a start Step requires None input")
|
|
117
|
+
if options.timeout is not None:
|
|
118
|
+
request.flow_timeout_seconds = self._seconds32(options.timeout)
|
|
119
|
+
response = cast(
|
|
120
|
+
pb.StartFlowResponse, self._call(self._service.StartFlow, request)
|
|
121
|
+
)
|
|
122
|
+
return response.run_id
|
|
123
|
+
|
|
124
|
+
@overload
|
|
125
|
+
def invoke_rpc(
|
|
126
|
+
self,
|
|
127
|
+
rpc_method: Callable[[Context, InputT], RPCResult[OutputT]],
|
|
128
|
+
flow_id: str,
|
|
129
|
+
input: InputT,
|
|
130
|
+
*,
|
|
131
|
+
run_id: str = "",
|
|
132
|
+
) -> OutputT: ...
|
|
133
|
+
|
|
134
|
+
@overload
|
|
135
|
+
def invoke_rpc(
|
|
136
|
+
self,
|
|
137
|
+
rpc_method: Callable[[Context], RPCResult[OutputT]],
|
|
138
|
+
flow_id: str,
|
|
139
|
+
*,
|
|
140
|
+
run_id: str = "",
|
|
141
|
+
) -> OutputT: ...
|
|
142
|
+
|
|
143
|
+
@overload
|
|
144
|
+
def invoke_rpc(
|
|
145
|
+
self,
|
|
146
|
+
rpc_method: Callable[[Context, InputT], None],
|
|
147
|
+
flow_id: str,
|
|
148
|
+
input: InputT,
|
|
149
|
+
*,
|
|
150
|
+
run_id: str = "",
|
|
151
|
+
) -> None: ...
|
|
152
|
+
|
|
153
|
+
@overload
|
|
154
|
+
def invoke_rpc(
|
|
155
|
+
self,
|
|
156
|
+
rpc_method: Callable[[Context], None],
|
|
157
|
+
flow_id: str,
|
|
158
|
+
*,
|
|
159
|
+
run_id: str = "",
|
|
160
|
+
) -> None: ...
|
|
161
|
+
|
|
162
|
+
def invoke_rpc(
|
|
163
|
+
self,
|
|
164
|
+
rpc_method: Callable[..., Any],
|
|
165
|
+
flow_id: str,
|
|
166
|
+
input: object = None,
|
|
167
|
+
*,
|
|
168
|
+
run_id: str = "",
|
|
169
|
+
) -> Any:
|
|
170
|
+
_, rpc = self.registry._rpc_for_method(rpc_method)
|
|
171
|
+
encoded_input = (
|
|
172
|
+
self._values.encode(input, rpc.input_codec)
|
|
173
|
+
if rpc.input_codec is not None
|
|
174
|
+
else self._values.encode_dynamic(None)
|
|
175
|
+
)
|
|
176
|
+
timeout = (
|
|
177
|
+
self._seconds32(rpc.options.timeout)
|
|
178
|
+
if rpc.options.timeout is not None
|
|
179
|
+
else 0
|
|
180
|
+
)
|
|
181
|
+
response = cast(
|
|
182
|
+
pb.InvokeRPCResponse,
|
|
183
|
+
self._call(
|
|
184
|
+
self._service.InvokeRPC,
|
|
185
|
+
pb.InvokeRPCRequest(
|
|
186
|
+
flow_id=require_name(flow_id),
|
|
187
|
+
run_id=run_id,
|
|
188
|
+
rpc_name=rpc.name,
|
|
189
|
+
input=encoded_input,
|
|
190
|
+
timeout_seconds=timeout,
|
|
191
|
+
lock_attribute_keys=rpc.locks,
|
|
192
|
+
request_id=str(uuid4()),
|
|
193
|
+
),
|
|
194
|
+
),
|
|
195
|
+
)
|
|
196
|
+
if rpc.output_codec is None:
|
|
197
|
+
return None
|
|
198
|
+
return self._values.decode(
|
|
199
|
+
self._hydrator.hydrate(response.output),
|
|
200
|
+
rpc.output_codec,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
@overload
|
|
204
|
+
def get_attribute(
|
|
205
|
+
self,
|
|
206
|
+
flow_id: str,
|
|
207
|
+
attribute: Attribute[ValueT],
|
|
208
|
+
*,
|
|
209
|
+
run_id: str = "",
|
|
210
|
+
) -> ValueT: ...
|
|
211
|
+
|
|
212
|
+
@overload
|
|
213
|
+
def get_attribute(
|
|
214
|
+
self,
|
|
215
|
+
flow_id: str,
|
|
216
|
+
attribute: AttributeMap[ValueT],
|
|
217
|
+
instance: str,
|
|
218
|
+
*,
|
|
219
|
+
run_id: str = "",
|
|
220
|
+
) -> ValueT: ...
|
|
221
|
+
|
|
222
|
+
def get_attribute(
|
|
223
|
+
self,
|
|
224
|
+
flow_id: str,
|
|
225
|
+
attribute: Attribute[Any] | AttributeMap[Any],
|
|
226
|
+
instance: str | None = None,
|
|
227
|
+
*,
|
|
228
|
+
run_id: str = "",
|
|
229
|
+
) -> Any:
|
|
230
|
+
key = self._definition_name(attribute, instance)
|
|
231
|
+
response = cast(
|
|
232
|
+
pb.GetAttributesResponse,
|
|
233
|
+
self._call(
|
|
234
|
+
self._service.GetAttributes,
|
|
235
|
+
pb.GetAttributesRequest(
|
|
236
|
+
flow_id=require_name(flow_id),
|
|
237
|
+
run_id=run_id,
|
|
238
|
+
keys=[key],
|
|
239
|
+
),
|
|
240
|
+
),
|
|
241
|
+
)
|
|
242
|
+
if not response.attributes:
|
|
243
|
+
return None
|
|
244
|
+
value = self._hydrator.hydrate(response.attributes[0].value)
|
|
245
|
+
return self._values.decode(value, self._values.codec(attribute.value_type))
|
|
246
|
+
|
|
247
|
+
@overload
|
|
248
|
+
def set_attribute(
|
|
249
|
+
self,
|
|
250
|
+
flow_id: str,
|
|
251
|
+
attribute: Attribute[ValueT],
|
|
252
|
+
value: ValueT,
|
|
253
|
+
/,
|
|
254
|
+
*,
|
|
255
|
+
run_id: str = "",
|
|
256
|
+
) -> None: ...
|
|
257
|
+
|
|
258
|
+
@overload
|
|
259
|
+
def set_attribute(
|
|
260
|
+
self,
|
|
261
|
+
flow_id: str,
|
|
262
|
+
attribute: AttributeMap[ValueT],
|
|
263
|
+
instance: str,
|
|
264
|
+
value: ValueT,
|
|
265
|
+
/,
|
|
266
|
+
*,
|
|
267
|
+
run_id: str = "",
|
|
268
|
+
) -> None: ...
|
|
269
|
+
|
|
270
|
+
def set_attribute(
|
|
271
|
+
self,
|
|
272
|
+
flow_id: str,
|
|
273
|
+
attribute: Attribute[Any] | AttributeMap[Any],
|
|
274
|
+
/,
|
|
275
|
+
*args: object,
|
|
276
|
+
run_id: str = "",
|
|
277
|
+
) -> None:
|
|
278
|
+
instance, value = self._definition_value(attribute, args)
|
|
279
|
+
write = pb.AttributeWrite(
|
|
280
|
+
key=self._definition_name(attribute, instance),
|
|
281
|
+
value=self._values.encode(
|
|
282
|
+
value,
|
|
283
|
+
self._values.codec(attribute.value_type),
|
|
284
|
+
),
|
|
285
|
+
)
|
|
286
|
+
index = self._values.index_config(
|
|
287
|
+
attribute.index,
|
|
288
|
+
isinstance(attribute, AttributeMap),
|
|
289
|
+
)
|
|
290
|
+
if index is not None:
|
|
291
|
+
write.index_config.CopyFrom(index)
|
|
292
|
+
self._call(
|
|
293
|
+
self._service.SetAttributes,
|
|
294
|
+
pb.SetAttributesRequest(
|
|
295
|
+
flow_id=require_name(flow_id),
|
|
296
|
+
run_id=run_id,
|
|
297
|
+
attributes=[write],
|
|
298
|
+
request_id=str(uuid4()),
|
|
299
|
+
),
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
@overload
|
|
303
|
+
def publish(
|
|
304
|
+
self,
|
|
305
|
+
flow_id: str,
|
|
306
|
+
channel: Channel[ValueT],
|
|
307
|
+
/,
|
|
308
|
+
*values: ValueT,
|
|
309
|
+
run_id: str = "",
|
|
310
|
+
) -> None: ...
|
|
311
|
+
|
|
312
|
+
@overload
|
|
313
|
+
def publish(
|
|
314
|
+
self,
|
|
315
|
+
flow_id: str,
|
|
316
|
+
channel: ChannelMap[ValueT],
|
|
317
|
+
instance: str,
|
|
318
|
+
/,
|
|
319
|
+
*values: ValueT,
|
|
320
|
+
run_id: str = "",
|
|
321
|
+
) -> None: ...
|
|
322
|
+
|
|
323
|
+
def publish(
|
|
324
|
+
self,
|
|
325
|
+
flow_id: str,
|
|
326
|
+
channel: Channel[Any] | ChannelMap[Any],
|
|
327
|
+
/,
|
|
328
|
+
*args: object,
|
|
329
|
+
run_id: str = "",
|
|
330
|
+
) -> None:
|
|
331
|
+
if isinstance(channel, ChannelMap):
|
|
332
|
+
if len(args) < 2 or not isinstance(args[0], str):
|
|
333
|
+
raise TypeError("ChannelMap publish requires instance and values")
|
|
334
|
+
instance = args[0]
|
|
335
|
+
values = args[1:]
|
|
336
|
+
else:
|
|
337
|
+
instance = None
|
|
338
|
+
values = args
|
|
339
|
+
if not values:
|
|
340
|
+
raise ValueError("publish requires at least one value")
|
|
341
|
+
name = self._definition_name(channel, instance)
|
|
342
|
+
codec = self._values.codec(channel.value_type)
|
|
343
|
+
self._call(
|
|
344
|
+
self._service.PublishToChannel,
|
|
345
|
+
pb.PublishToChannelRequest(
|
|
346
|
+
flow_id=require_name(flow_id),
|
|
347
|
+
run_id=run_id,
|
|
348
|
+
messages=[
|
|
349
|
+
pb.ChannelMessage(
|
|
350
|
+
channel_name=name,
|
|
351
|
+
value=self._values.encode(value, codec),
|
|
352
|
+
)
|
|
353
|
+
for value in values
|
|
354
|
+
],
|
|
355
|
+
),
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
@overload
|
|
359
|
+
def wait_for_flow(self, flow_id: str) -> None: ...
|
|
360
|
+
|
|
361
|
+
@overload
|
|
362
|
+
def wait_for_flow(
|
|
363
|
+
self,
|
|
364
|
+
flow_id: str,
|
|
365
|
+
output_type: type[OutputT],
|
|
366
|
+
timeout: timedelta | None = None,
|
|
367
|
+
) -> OutputT: ...
|
|
368
|
+
|
|
369
|
+
def wait_for_flow(
|
|
370
|
+
self,
|
|
371
|
+
flow_id: str,
|
|
372
|
+
output_type: type[Any] | None = None,
|
|
373
|
+
timeout: timedelta | None = None,
|
|
374
|
+
) -> Any:
|
|
375
|
+
response = self._wait_for_flow_response(flow_id, timeout)
|
|
376
|
+
if output_type is None:
|
|
377
|
+
return None
|
|
378
|
+
codec = self._values.codec(output_type)
|
|
379
|
+
for result in reversed(response.results):
|
|
380
|
+
if result.HasField("completed_step_output"):
|
|
381
|
+
return self._values.decode(
|
|
382
|
+
self._hydrator.hydrate(result.completed_step_output),
|
|
383
|
+
codec,
|
|
384
|
+
)
|
|
385
|
+
return None
|
|
386
|
+
|
|
387
|
+
def stop_flow(
|
|
388
|
+
self,
|
|
389
|
+
flow_id: str,
|
|
390
|
+
options: StopFlowOptions = StopFlowOptions(),
|
|
391
|
+
) -> None:
|
|
392
|
+
self._call(
|
|
393
|
+
self._service.StopFlow,
|
|
394
|
+
pb.StopFlowRequest(
|
|
395
|
+
flow_id=require_name(flow_id),
|
|
396
|
+
reason=options.reason or "",
|
|
397
|
+
stop_type={
|
|
398
|
+
StopType.CANCEL: pb.STOP_TYPE_CANCEL,
|
|
399
|
+
StopType.TERMINATE: pb.STOP_TYPE_TERMINATE,
|
|
400
|
+
StopType.FAIL: pb.STOP_TYPE_FAIL,
|
|
401
|
+
}[options.type],
|
|
402
|
+
),
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
def describe_flow(self, flow_id: str) -> FlowInfo:
|
|
406
|
+
response = cast(
|
|
407
|
+
pb.GetFlowSummaryResponse,
|
|
408
|
+
self._call(
|
|
409
|
+
self._service.GetFlowSummary,
|
|
410
|
+
pb.GetFlowSummaryRequest(flow_id=require_name(flow_id)),
|
|
411
|
+
),
|
|
412
|
+
)
|
|
413
|
+
return FlowInfo(
|
|
414
|
+
response.flow_execution_id.flow_id,
|
|
415
|
+
response.flow_execution_id.run_id,
|
|
416
|
+
response.flow_type,
|
|
417
|
+
self._map_flow_status(response.flow_status),
|
|
418
|
+
response.start_time.ToDatetime(tzinfo=timezone.utc),
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
def reset_flow(self, flow_id: str, options: ResetFlowOptions) -> str:
|
|
422
|
+
request = pb.ResetFlowRequest(
|
|
423
|
+
flow_id=require_name(flow_id),
|
|
424
|
+
reset_type={
|
|
425
|
+
ResetType.BEGINNING: pb.FLOW_RESET_TYPE_BEGINNING,
|
|
426
|
+
ResetType.HISTORY_EVENT_ID: pb.FLOW_RESET_TYPE_HISTORY_EVENT_ID,
|
|
427
|
+
ResetType.HISTORY_EVENT_TIME: pb.FLOW_RESET_TYPE_HISTORY_EVENT_TIME,
|
|
428
|
+
ResetType.STEP_TYPE: pb.FLOW_RESET_TYPE_STEP_TYPE,
|
|
429
|
+
ResetType.STEP_EXECUTION_ID: pb.FLOW_RESET_TYPE_STEP_EXECUTION_ID,
|
|
430
|
+
}[options.type],
|
|
431
|
+
reason=options.reason or "",
|
|
432
|
+
skip_channel_messages_reapply=options.skip_channel_messages_reapply,
|
|
433
|
+
skip_locking_rpc_reapply=options.skip_locking_rpc_reapply,
|
|
434
|
+
)
|
|
435
|
+
if options.history_event_id is not None:
|
|
436
|
+
request.history_event_id = options.history_event_id
|
|
437
|
+
if options.history_event_time is not None:
|
|
438
|
+
request.history_event_time = options.history_event_time.isoformat()
|
|
439
|
+
if options.step_type is not None:
|
|
440
|
+
request.step_type = options.step_type
|
|
441
|
+
if options.step_execution_id is not None:
|
|
442
|
+
request.step_execution_id = options.step_execution_id
|
|
443
|
+
response = cast(
|
|
444
|
+
pb.ResetFlowResponse,
|
|
445
|
+
self._call(self._service.ResetFlow, request),
|
|
446
|
+
)
|
|
447
|
+
return response.run_id
|
|
448
|
+
|
|
449
|
+
def skip_timer(
|
|
450
|
+
self,
|
|
451
|
+
flow_id: str,
|
|
452
|
+
step_execution_id: StepExecutionId,
|
|
453
|
+
timer_id: TimerId,
|
|
454
|
+
) -> None:
|
|
455
|
+
request = pb.SkipTimerRequest(
|
|
456
|
+
flow_id=require_name(flow_id),
|
|
457
|
+
step_execution_id=(
|
|
458
|
+
f"{step_execution_id.step_type}-{step_execution_id.number}"
|
|
459
|
+
),
|
|
460
|
+
)
|
|
461
|
+
if timer_id.condition_id is not None:
|
|
462
|
+
request.timer_condition_id = timer_id.condition_id
|
|
463
|
+
if timer_id.condition_index is not None:
|
|
464
|
+
request.timer_condition_index = timer_id.condition_index
|
|
465
|
+
self._call(self._service.SkipTimer, request)
|
|
466
|
+
|
|
467
|
+
def wait_for_step_completion(
|
|
468
|
+
self,
|
|
469
|
+
flow_id: str,
|
|
470
|
+
step_execution_id: StepExecutionId,
|
|
471
|
+
timeout: timedelta,
|
|
472
|
+
) -> None:
|
|
473
|
+
self._call(
|
|
474
|
+
self._service.WaitForStepCompletion,
|
|
475
|
+
pb.WaitForStepCompletionRequest(
|
|
476
|
+
flow_id=require_name(flow_id),
|
|
477
|
+
step_type=step_execution_id.step_type,
|
|
478
|
+
step_execution_number=str(step_execution_id.number),
|
|
479
|
+
wait_time_seconds=self._seconds32(timeout),
|
|
480
|
+
request_id=str(uuid4()),
|
|
481
|
+
),
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
def update_flow_config(self, flow_id: str, config: FlowConfig) -> None:
|
|
485
|
+
self._call(
|
|
486
|
+
self._service.UpdateFlowConfig,
|
|
487
|
+
pb.UpdateFlowConfigRequest(
|
|
488
|
+
flow_id=require_name(flow_id),
|
|
489
|
+
flow_config=self._map_flow_config(config),
|
|
490
|
+
),
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
def trigger_continue_as_new(self, flow_id: str) -> None:
|
|
494
|
+
self._call(
|
|
495
|
+
self._service.TriggerContinueAsNew,
|
|
496
|
+
pb.TriggerContinueAsNewRequest(flow_id=require_name(flow_id)),
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
def health_check(self) -> bool:
|
|
500
|
+
from google.protobuf import empty_pb2
|
|
501
|
+
|
|
502
|
+
self._call(self._service.HealthCheck, empty_pb2.Empty())
|
|
503
|
+
return True
|
|
504
|
+
|
|
505
|
+
def close(self) -> None:
|
|
506
|
+
if self._closed:
|
|
507
|
+
return
|
|
508
|
+
self._closed = True
|
|
509
|
+
self._channel.close()
|
|
510
|
+
|
|
511
|
+
def _wait_for_flow_response(
|
|
512
|
+
self,
|
|
513
|
+
flow_id: str,
|
|
514
|
+
timeout: timedelta | None,
|
|
515
|
+
) -> pb.WaitForFlowResponse:
|
|
516
|
+
request = pb.WaitForFlowRequest(
|
|
517
|
+
flow_id=require_name(flow_id),
|
|
518
|
+
needs_results=True,
|
|
519
|
+
)
|
|
520
|
+
if timeout is not None:
|
|
521
|
+
request.wait_time_seconds = self._seconds32(timeout)
|
|
522
|
+
try:
|
|
523
|
+
response = cast(pb.WaitForFlowResponse, self._service.WaitForFlow(request))
|
|
524
|
+
except grpc.RpcError as error:
|
|
525
|
+
if error.code() is grpc.StatusCode.DEADLINE_EXCEEDED:
|
|
526
|
+
raise LongPollTimeoutError(flow_id) from error
|
|
527
|
+
raise translate_rpc_error(error) from error
|
|
528
|
+
if response.flow_status != pb.FLOW_STATUS_COMPLETED:
|
|
529
|
+
info = self.describe_flow(flow_id)
|
|
530
|
+
results = self._hydrator.step_outputs(list(response.results))
|
|
531
|
+
raise FlowUncompletedError(
|
|
532
|
+
info.run_id,
|
|
533
|
+
self._map_flow_status(response.flow_status),
|
|
534
|
+
self._map_flow_error_type(response.error_type),
|
|
535
|
+
response.error_message or None,
|
|
536
|
+
results,
|
|
537
|
+
self._values,
|
|
538
|
+
)
|
|
539
|
+
return response
|
|
540
|
+
|
|
541
|
+
def _map_start_options(self, options: StartFlowOptions) -> pb.FlowStartOptions:
|
|
542
|
+
mapped = pb.FlowStartOptions(
|
|
543
|
+
id_reuse_policy={
|
|
544
|
+
IdReusePolicy.DEFAULT: pb.ID_REUSE_POLICY_UNSPECIFIED,
|
|
545
|
+
IdReusePolicy.ALLOW_IF_PREVIOUS_FAILED: (
|
|
546
|
+
pb.ID_REUSE_POLICY_ALLOW_IF_PREVIOUS_EXISTS_ABNORMALLY
|
|
547
|
+
),
|
|
548
|
+
IdReusePolicy.ALLOW_IF_NOT_RUNNING: (
|
|
549
|
+
pb.ID_REUSE_POLICY_ALLOW_IF_NO_RUNNING
|
|
550
|
+
),
|
|
551
|
+
IdReusePolicy.ALLOW_TERMINATE_IF_RUNNING: (
|
|
552
|
+
pb.ID_REUSE_POLICY_ALLOW_TERMINATE_IF_RUNNING
|
|
553
|
+
),
|
|
554
|
+
IdReusePolicy.DISALLOW: pb.ID_REUSE_POLICY_DISALLOW_REUSE,
|
|
555
|
+
}[options.id_reuse_policy],
|
|
556
|
+
cron_schedule=options.cron_schedule or "",
|
|
557
|
+
flow_already_started_options=pb.FlowAlreadyStartedOptions(
|
|
558
|
+
ignore_already_started_error=options.ignore_already_started
|
|
559
|
+
),
|
|
560
|
+
)
|
|
561
|
+
if options.start_delay is not None:
|
|
562
|
+
mapped.flow_start_delay_seconds = self._seconds32(options.start_delay)
|
|
563
|
+
if options.retry_policy is not None:
|
|
564
|
+
mapped.retry_policy.CopyFrom(self._map_flow_retry(options.retry_policy))
|
|
565
|
+
for initialization in options._attribute_initializations:
|
|
566
|
+
definition = initialization.definition
|
|
567
|
+
key = self._definition_name(definition, initialization.instance)
|
|
568
|
+
mapped.attributes.append(
|
|
569
|
+
pb.AttributeWrite(
|
|
570
|
+
key=key,
|
|
571
|
+
value=self._values.encode(
|
|
572
|
+
initialization.value,
|
|
573
|
+
self._values.codec(definition.value_type),
|
|
574
|
+
),
|
|
575
|
+
)
|
|
576
|
+
)
|
|
577
|
+
if (
|
|
578
|
+
options.config_override is not None
|
|
579
|
+
or self.options.worker_target is not None
|
|
580
|
+
):
|
|
581
|
+
mapped.flow_config_override.CopyFrom(
|
|
582
|
+
self._map_flow_config(options.config_override)
|
|
583
|
+
)
|
|
584
|
+
return mapped
|
|
585
|
+
|
|
586
|
+
def _map_flow_config(self, config: FlowConfig | None) -> pb.FlowConfig:
|
|
587
|
+
mapped = pb.FlowConfig()
|
|
588
|
+
if config is not None:
|
|
589
|
+
if config.active_step_search_mode is not None:
|
|
590
|
+
mapped.active_step_search_mode = {
|
|
591
|
+
ActiveStepSearchMode.DEFAULT: (
|
|
592
|
+
pb.ACTIVE_STEP_SEARCH_MODE_UNSPECIFIED
|
|
593
|
+
),
|
|
594
|
+
ActiveStepSearchMode.ALL: (
|
|
595
|
+
pb.ACTIVE_STEP_SEARCH_MODE_ENABLED_FOR_ALL
|
|
596
|
+
),
|
|
597
|
+
ActiveStepSearchMode.WITH_WAIT_FOR: (
|
|
598
|
+
pb.ACTIVE_STEP_SEARCH_MODE_ENABLED_FOR_STEPS_WITH_WAIT_FOR
|
|
599
|
+
),
|
|
600
|
+
ActiveStepSearchMode.DISABLED: (
|
|
601
|
+
pb.ACTIVE_STEP_SEARCH_MODE_DISABLED
|
|
602
|
+
),
|
|
603
|
+
}[config.active_step_search_mode]
|
|
604
|
+
if config.continue_as_new_threshold is not None:
|
|
605
|
+
mapped.continue_as_new_threshold = config.continue_as_new_threshold
|
|
606
|
+
if config.continue_as_new_page_size_bytes is not None:
|
|
607
|
+
mapped.continue_as_new_page_size_in_bytes = (
|
|
608
|
+
config.continue_as_new_page_size_bytes
|
|
609
|
+
)
|
|
610
|
+
if config.step_durability is not None:
|
|
611
|
+
mapped.step_durability = {
|
|
612
|
+
StepDurability.DEFAULT: pb.STEP_DURABILITY_UNSPECIFIED,
|
|
613
|
+
StepDurability.SYNC: pb.STEP_DURABILITY_SYNC,
|
|
614
|
+
StepDurability.ASYNC: pb.STEP_DURABILITY_ASYNC,
|
|
615
|
+
}[config.step_durability]
|
|
616
|
+
target = (
|
|
617
|
+
config.worker_target
|
|
618
|
+
if config is not None and config.worker_target is not None
|
|
619
|
+
else self.options.worker_target
|
|
620
|
+
)
|
|
621
|
+
if target is not None:
|
|
622
|
+
mapped.worker_target.CopyFrom(
|
|
623
|
+
pb.WorkerTarget(
|
|
624
|
+
address=target.address,
|
|
625
|
+
is_headless_address=target.headless,
|
|
626
|
+
)
|
|
627
|
+
)
|
|
628
|
+
return mapped
|
|
629
|
+
|
|
630
|
+
@staticmethod
|
|
631
|
+
def _map_flow_retry(retry: RetryPolicy) -> pb.FlowRetryPolicy:
|
|
632
|
+
mapped = pb.FlowRetryPolicy(
|
|
633
|
+
backoff_coefficient=retry.backoff_coefficient,
|
|
634
|
+
maximum_attempts=retry.maximum_attempts,
|
|
635
|
+
)
|
|
636
|
+
if retry.initial_interval is not None:
|
|
637
|
+
mapped.initial_interval_seconds = Client._seconds32(retry.initial_interval)
|
|
638
|
+
if retry.maximum_interval is not None:
|
|
639
|
+
mapped.maximum_interval_seconds = Client._seconds32(retry.maximum_interval)
|
|
640
|
+
return mapped
|
|
641
|
+
|
|
642
|
+
@staticmethod
|
|
643
|
+
def _definition_name(
|
|
644
|
+
definition: Attribute[Any] | AttributeMap[Any] | Channel[Any] | ChannelMap[Any],
|
|
645
|
+
instance: str | None,
|
|
646
|
+
) -> str:
|
|
647
|
+
if isinstance(definition, (AttributeMap, ChannelMap)):
|
|
648
|
+
if instance is None:
|
|
649
|
+
raise ValueError("dynamic definition requires an instance")
|
|
650
|
+
return Registry.physical_name(definition.name, instance)
|
|
651
|
+
if instance is not None:
|
|
652
|
+
raise ValueError("static definition cannot use an instance")
|
|
653
|
+
return definition.name
|
|
654
|
+
|
|
655
|
+
@staticmethod
|
|
656
|
+
def _definition_value(
|
|
657
|
+
definition: Attribute[Any] | AttributeMap[Any],
|
|
658
|
+
args: tuple[object, ...],
|
|
659
|
+
) -> tuple[str | None, object]:
|
|
660
|
+
if isinstance(definition, Attribute) and len(args) == 1:
|
|
661
|
+
return None, args[0]
|
|
662
|
+
if (
|
|
663
|
+
isinstance(definition, AttributeMap)
|
|
664
|
+
and len(args) == 2
|
|
665
|
+
and isinstance(args[0], str)
|
|
666
|
+
):
|
|
667
|
+
return args[0], args[1]
|
|
668
|
+
raise TypeError("set_attribute received invalid arguments")
|
|
669
|
+
|
|
670
|
+
@staticmethod
|
|
671
|
+
def _seconds32(duration: timedelta) -> int:
|
|
672
|
+
seconds = duration.total_seconds()
|
|
673
|
+
if seconds < 0 or not seconds.is_integer() or seconds > 2**31 - 1:
|
|
674
|
+
raise ValueError("duration must be whole seconds within int32")
|
|
675
|
+
return int(seconds)
|
|
676
|
+
|
|
677
|
+
@staticmethod
|
|
678
|
+
def _map_flow_status(status: int) -> FlowStatus:
|
|
679
|
+
statuses: dict[int, FlowStatus] = {
|
|
680
|
+
int(pb.FLOW_STATUS_RUNNING): FlowStatus.RUNNING,
|
|
681
|
+
int(pb.FLOW_STATUS_COMPLETED): FlowStatus.COMPLETED,
|
|
682
|
+
int(pb.FLOW_STATUS_FAILED): FlowStatus.FAILED,
|
|
683
|
+
int(pb.FLOW_STATUS_TIMEOUT): FlowStatus.TIMED_OUT,
|
|
684
|
+
int(pb.FLOW_STATUS_TERMINATED): FlowStatus.TERMINATED,
|
|
685
|
+
int(pb.FLOW_STATUS_CANCELED): FlowStatus.CANCELED,
|
|
686
|
+
int(pb.FLOW_STATUS_CONTINUED_AS_NEW): FlowStatus.CONTINUED_AS_NEW,
|
|
687
|
+
}
|
|
688
|
+
try:
|
|
689
|
+
return statuses[status]
|
|
690
|
+
except KeyError as error:
|
|
691
|
+
raise ValueError(f"unknown Flow status {status}") from error
|
|
692
|
+
|
|
693
|
+
@staticmethod
|
|
694
|
+
def _map_flow_error_type(error_type: int) -> FlowErrorType | None:
|
|
695
|
+
error_types: dict[int, FlowErrorType] = {
|
|
696
|
+
int(pb.FLOW_ERROR_TYPE_STEP_DECISION_FAILING_FLOW): (
|
|
697
|
+
FlowErrorType.STEP_DECISION_FAILED
|
|
698
|
+
),
|
|
699
|
+
int(
|
|
700
|
+
pb.FLOW_ERROR_TYPE_CLIENT_API_FAILING_FLOW
|
|
701
|
+
): FlowErrorType.CLIENT_API_FAILED,
|
|
702
|
+
int(pb.FLOW_ERROR_TYPE_WORKER_API_FAIL): FlowErrorType.WORKER_API_FAILED,
|
|
703
|
+
int(pb.FLOW_ERROR_TYPE_INVALID_USER_FLOW_CODE): (
|
|
704
|
+
FlowErrorType.INVALID_USER_FLOW_CODE
|
|
705
|
+
),
|
|
706
|
+
int(pb.FLOW_ERROR_TYPE_INTERNAL): FlowErrorType.INTERNAL,
|
|
707
|
+
}
|
|
708
|
+
return error_types.get(error_type)
|
|
709
|
+
|
|
710
|
+
@staticmethod
|
|
711
|
+
def _call(method: Callable[[Any], Any], request: Any) -> Any:
|
|
712
|
+
try:
|
|
713
|
+
return method(request)
|
|
714
|
+
except grpc.RpcError as error:
|
|
715
|
+
raise translate_rpc_error(error) from error
|