dex-python-sdk 0.0.1__py3-none-any.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 +14 -0
- dex/client.py +568 -0
- dex/client_options.py +34 -0
- dex/command_request.py +130 -0
- dex/command_results.py +116 -0
- dex/communication.py +141 -0
- dex/communication_schema.py +68 -0
- dex/data_attributes.py +75 -0
- dex/dexpb/__init__.py +1 -0
- dex/dexpb/dex_pb2.py +305 -0
- dex/dexpb/dex_pb2.pyi +1273 -0
- dex/dexpb/dex_pb2_grpc.py +1126 -0
- dex/errors.py +132 -0
- dex/object_encoder.py +818 -0
- dex/persistence.py +94 -0
- dex/persistence_options.py +24 -0
- dex/persistence_schema.py +58 -0
- dex/py.typed +1 -0
- dex/registry.py +209 -0
- dex/reset_workflow_type_and_options.py +77 -0
- dex/rpc.py +100 -0
- dex/search_attributes.py +189 -0
- dex/state_decision.py +161 -0
- dex/state_execution_locals.py +71 -0
- dex/state_movement.py +123 -0
- dex/state_schema.py +54 -0
- dex/stop_workflow_options.py +23 -0
- dex/tests/__init__.py +84 -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 +87 -0
- dex/tests/dex-service-env/dynamicconfig/README.md +39 -0
- dex/tests/dex-service-env/dynamicconfig/development-sql.yaml +6 -0
- dex/tests/dex-service-env/dynamicconfig/docker.yaml +0 -0
- dex/tests/test_abnormal_exit_workflow.py +48 -0
- dex/tests/test_basic_workflow.py +75 -0
- dex/tests/test_conditional_complete.py +56 -0
- dex/tests/test_describe_workflow.py +45 -0
- dex/tests/test_empty_data_decodes_properly.py +79 -0
- dex/tests/test_internal_channel.py +33 -0
- dex/tests/test_internal_channel_with_no_prefix_channel.py +46 -0
- dex/tests/test_persistence_data_attributes.py +67 -0
- dex/tests/test_persistence_search_attributes.py +132 -0
- dex/tests/test_persistence_state_execution_locals.py +43 -0
- dex/tests/test_rpc.py +69 -0
- dex/tests/test_rpc_with_memo.py +200 -0
- dex/tests/test_rpc_with_memo_duplicate_java_tests.py +122 -0
- dex/tests/test_signal.py +56 -0
- dex/tests/test_skip_wait_until.py +85 -0
- dex/tests/test_state_failure_recovery.py +33 -0
- dex/tests/test_timer.py +40 -0
- dex/tests/test_wait_for_state_execution_completion.py +58 -0
- dex/tests/test_workflow_errors.py +92 -0
- dex/tests/test_workflow_state_options.py +123 -0
- dex/tests/test_workflow_state_options_override.py +49 -0
- dex/tests/worker_server.py +74 -0
- dex/tests/workflows/abnormal_exit_workflow.py +48 -0
- dex/tests/workflows/basic_workflow.py +69 -0
- dex/tests/workflows/conditional_complete_workflow.py +101 -0
- dex/tests/workflows/describe_workflow.py +52 -0
- dex/tests/workflows/empty_data_workflow.py +51 -0
- dex/tests/workflows/internal_channel_workflow.py +137 -0
- dex/tests/workflows/internal_channel_workflow_with_no_prefix_channel.py +108 -0
- dex/tests/workflows/java_duplicate_rpc_memo_workflow.py +285 -0
- dex/tests/workflows/persistence_data_attributes_workflow.py +104 -0
- dex/tests/workflows/persistence_search_attributes_workflow.py +167 -0
- dex/tests/workflows/persistence_state_execution_local_workflow.py +69 -0
- dex/tests/workflows/recovery_workflow.py +90 -0
- dex/tests/workflows/rpc_memo_workflow.py +238 -0
- dex/tests/workflows/rpc_workflow.py +125 -0
- dex/tests/workflows/state_options_override_workflow.py +100 -0
- dex/tests/workflows/state_options_workflow.py +92 -0
- dex/tests/workflows/timer_workflow.py +52 -0
- dex/tests/workflows/wait_for_state_with_state_execution_id_workflow.py +77 -0
- dex/tests/workflows/wait_for_state_with_wait_for_key_workflow.py +78 -0
- dex/tests/workflows/wait_internal_channel_workflow.py +53 -0
- dex/tests/workflows/wait_signal_workflow.py +154 -0
- dex/type_store.py +105 -0
- dex/unregistered_client.py +595 -0
- dex/utils/__init__.py +14 -0
- dex/utils/dex_typing.py +31 -0
- dex/utils/persistence_utils.py +37 -0
- dex/worker_service.py +441 -0
- dex/workflow.py +86 -0
- dex/workflow_context.py +50 -0
- dex/workflow_info.py +21 -0
- dex/workflow_options.py +79 -0
- dex/workflow_state.py +134 -0
- dex/workflow_state_options.py +160 -0
- dex_python_sdk-0.0.1.dist-info/METADATA +140 -0
- dex_python_sdk-0.0.1.dist-info/RECORD +93 -0
- dex_python_sdk-0.0.1.dist-info/WHEEL +4 -0
- dex_python_sdk-0.0.1.dist-info/licenses/LICENSE +201 -0
dex/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Copyright (c) 2022-2026 Super Durable, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
dex/client.py
ADDED
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
# Copyright (c) 2022-2026 Super Durable, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import inspect
|
|
16
|
+
from typing import Any, Callable, List, Optional, Type, TypeVar, Union
|
|
17
|
+
|
|
18
|
+
from typing_extensions import deprecated
|
|
19
|
+
|
|
20
|
+
from dex.client_options import ClientOptions
|
|
21
|
+
from dex.errors import InvalidArgumentError, NotRegisteredError, WorkflowDefinitionError
|
|
22
|
+
from dex.dex_api.models import (
|
|
23
|
+
SearchAttribute,
|
|
24
|
+
SearchAttributeKeyAndType,
|
|
25
|
+
SearchAttributeValueType,
|
|
26
|
+
)
|
|
27
|
+
from dex.dex_api.types import Unset
|
|
28
|
+
from dex.registry import Registry
|
|
29
|
+
from dex.reset_workflow_type_and_options import ResetWorkflowTypeAndOptions
|
|
30
|
+
from dex.stop_workflow_options import StopWorkflowOptions
|
|
31
|
+
from dex.unregistered_client import UnregisteredClient, UnregisteredWorkflowOptions
|
|
32
|
+
from dex.utils.dex_typing import unset_to_none
|
|
33
|
+
from dex.utils.persistence_utils import get_search_attribute_value
|
|
34
|
+
from dex.workflow import ObjectWorkflow, get_workflow_type_by_class
|
|
35
|
+
from dex.workflow_info import WorkflowInfo
|
|
36
|
+
from dex.workflow_options import WorkflowOptions
|
|
37
|
+
from dex.workflow_state import (
|
|
38
|
+
WorkflowState,
|
|
39
|
+
get_state_id,
|
|
40
|
+
get_state_id_by_class,
|
|
41
|
+
get_state_execution_id,
|
|
42
|
+
should_skip_wait_until,
|
|
43
|
+
)
|
|
44
|
+
from dex.workflow_state_options import _to_idl_state_options
|
|
45
|
+
|
|
46
|
+
T = TypeVar("T")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_workflow_type_by_rpc_method(meth) -> str:
|
|
50
|
+
if inspect.ismethod(meth):
|
|
51
|
+
return inspect.getmro(meth.__self__.__class__)[0].__name__
|
|
52
|
+
if inspect.isfunction(meth):
|
|
53
|
+
return meth.__qualname__.split(".<locals>", 1)[0].rsplit(".", 1)[0]
|
|
54
|
+
raise InvalidArgumentError(f"method {meth} is not a RPC method")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Client:
|
|
58
|
+
def __init__(self, registry: Registry, options: Optional[ClientOptions] = None):
|
|
59
|
+
self._registry = registry
|
|
60
|
+
if options is None:
|
|
61
|
+
options = ClientOptions.local_default()
|
|
62
|
+
self._options = options
|
|
63
|
+
self._unregistered_client = UnregisteredClient(options)
|
|
64
|
+
|
|
65
|
+
def start_workflow(
|
|
66
|
+
self,
|
|
67
|
+
wf_class: type[ObjectWorkflow],
|
|
68
|
+
wf_id: str,
|
|
69
|
+
timeout_seconds: int,
|
|
70
|
+
input: Any = None,
|
|
71
|
+
options: Optional[WorkflowOptions] = None,
|
|
72
|
+
) -> str:
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
wf_class: the workflow definition class
|
|
77
|
+
wf_id: workflowId
|
|
78
|
+
timeout_seconds: the timeout. Use zero for infinite timeout(only works for Temporal as backend)
|
|
79
|
+
input: input of the workflow, aka, the input of the starting state of the workflow
|
|
80
|
+
options: advanced options
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
workflow_run_id: the run id of the started workflow
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
ClientSideError for non-retryable error
|
|
87
|
+
ServerSideError for server error
|
|
88
|
+
"""
|
|
89
|
+
wf_type = get_workflow_type_by_class(wf_class)
|
|
90
|
+
self._registry.get_workflow_with_check(wf_type)
|
|
91
|
+
|
|
92
|
+
starting_state = self._registry.get_workflow_starting_state(wf_type)
|
|
93
|
+
unreg_opts = UnregisteredWorkflowOptions()
|
|
94
|
+
|
|
95
|
+
if options is not None:
|
|
96
|
+
unreg_opts.workflow_id_reuse_policy = options.workflow_id_reuse_policy
|
|
97
|
+
unreg_opts.workflow_retry_policy = options.workflow_retry_policy
|
|
98
|
+
unreg_opts.cron_schedule = options.workflow_cron_schedule
|
|
99
|
+
unreg_opts.workflow_start_delay_seconds = (
|
|
100
|
+
options.workflow_start_delay_seconds
|
|
101
|
+
)
|
|
102
|
+
unreg_opts.workflow_already_started_options = (
|
|
103
|
+
options.workflow_already_started_options
|
|
104
|
+
)
|
|
105
|
+
unreg_opts.initial_data_attributes = options.initial_data_attributes
|
|
106
|
+
|
|
107
|
+
unreg_opts.workflow_config_override = options.workflow_config_override
|
|
108
|
+
|
|
109
|
+
unreg_opts.wait_for_completion_state_ids = (
|
|
110
|
+
options.wait_for_completion_state_ids
|
|
111
|
+
)
|
|
112
|
+
unreg_opts.wait_for_completion_state_execution_ids = (
|
|
113
|
+
options.wait_for_completion_state_execution_ids
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if options.initial_search_attributes:
|
|
117
|
+
sa_types = self._registry.get_search_attribute_types(wf_type)
|
|
118
|
+
converted_sas = convert_to_sa_list(
|
|
119
|
+
sa_types, options.initial_search_attributes
|
|
120
|
+
)
|
|
121
|
+
unreg_opts.initial_search_attributes = converted_sas
|
|
122
|
+
|
|
123
|
+
schema_options = self._registry.get_persistence_options(wf_type)
|
|
124
|
+
if schema_options.enable_caching:
|
|
125
|
+
unreg_opts.using_memo_for_data_attributes = schema_options.enable_caching
|
|
126
|
+
|
|
127
|
+
starting_state_id = None
|
|
128
|
+
|
|
129
|
+
if starting_state is not None:
|
|
130
|
+
starting_state_id = get_state_id(starting_state)
|
|
131
|
+
starting_state_opts = _to_idl_state_options(
|
|
132
|
+
should_skip_wait_until(starting_state),
|
|
133
|
+
starting_state.get_state_options(),
|
|
134
|
+
self._registry.get_state_store(wf_type),
|
|
135
|
+
)
|
|
136
|
+
unreg_opts.start_state_options = starting_state_opts
|
|
137
|
+
|
|
138
|
+
return self._unregistered_client.start_workflow(
|
|
139
|
+
wf_type, wf_id, starting_state_id, timeout_seconds, input, unreg_opts
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
@deprecated("use wait_for_workflow_completion instead")
|
|
143
|
+
def get_simple_workflow_result_with_wait(
|
|
144
|
+
self,
|
|
145
|
+
workflow_id: str,
|
|
146
|
+
type_hint: Optional[Type[T]] = None,
|
|
147
|
+
) -> Optional[T]:
|
|
148
|
+
return self._unregistered_client.get_simple_workflow_result_with_wait(
|
|
149
|
+
workflow_id, "", type_hint
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def wait_for_workflow_completion(
|
|
153
|
+
self,
|
|
154
|
+
workflow_id: str,
|
|
155
|
+
type_hint: Optional[Type[T]] = None,
|
|
156
|
+
) -> Optional[T]:
|
|
157
|
+
"""
|
|
158
|
+
This will be waiting up to 5~60 seconds (configurable in HTTP client and capped by server) for workflow to
|
|
159
|
+
complete, and return the workflow completion result.
|
|
160
|
+
Args:
|
|
161
|
+
workflow_id: workflowId
|
|
162
|
+
type_hint: the type of workflow result
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
the completion result if there is one
|
|
166
|
+
Raises
|
|
167
|
+
WorkflowAbnormalExitError if workflow failed/timeout/canceled/terminated
|
|
168
|
+
WorkflowStillRunningError if workflow is still running after exceeding the waiting timeout(HTTP timeout)
|
|
169
|
+
ClientSideError for non-retryable error
|
|
170
|
+
ServerSideError for server error
|
|
171
|
+
"""
|
|
172
|
+
return self._unregistered_client.get_simple_workflow_result_with_wait(
|
|
173
|
+
workflow_id, "", type_hint
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def stop_workflow(
|
|
177
|
+
self,
|
|
178
|
+
workflow_id: str,
|
|
179
|
+
options: Optional[StopWorkflowOptions] = None,
|
|
180
|
+
):
|
|
181
|
+
return self._unregistered_client.stop_workflow(workflow_id, "", options)
|
|
182
|
+
|
|
183
|
+
def get_all_workflow_data_attributes(
|
|
184
|
+
self,
|
|
185
|
+
workflow_class: type[ObjectWorkflow],
|
|
186
|
+
workflow_id: str,
|
|
187
|
+
workflow_run_id: str = "",
|
|
188
|
+
):
|
|
189
|
+
return self.get_workflow_data_attributes(
|
|
190
|
+
workflow_class, workflow_id, workflow_run_id, None
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
def get_workflow_data_attributes(
|
|
194
|
+
self,
|
|
195
|
+
workflow_class: type[ObjectWorkflow],
|
|
196
|
+
workflow_id: str,
|
|
197
|
+
workflow_run_id: str = "",
|
|
198
|
+
keys: Optional[List[str]] = None,
|
|
199
|
+
):
|
|
200
|
+
wf_type = get_workflow_type_by_class(workflow_class)
|
|
201
|
+
data_attr_type_store = self._registry.get_data_attribute_types(wf_type)
|
|
202
|
+
if keys:
|
|
203
|
+
for key in keys:
|
|
204
|
+
if not data_attr_type_store.is_valid_name_or_prefix(key):
|
|
205
|
+
raise NotRegisteredError(
|
|
206
|
+
f"key {key} is not registered in workflow {wf_type}"
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
schema_options = self._registry.get_persistence_options(wf_type)
|
|
210
|
+
|
|
211
|
+
response = self._unregistered_client.get_workflow_data_attributes(
|
|
212
|
+
workflow_id, workflow_run_id, keys, schema_options.enable_caching
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if not response.objects:
|
|
216
|
+
raise RuntimeError("data attributes not returned")
|
|
217
|
+
|
|
218
|
+
res = {}
|
|
219
|
+
for kv in response.objects:
|
|
220
|
+
k = unset_to_none(kv.key)
|
|
221
|
+
if k and kv.value:
|
|
222
|
+
res[kv.key] = self._options.object_encoder.decode(
|
|
223
|
+
kv.value, data_attr_type_store.get_type(k)
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
return res
|
|
227
|
+
|
|
228
|
+
def set_workflow_data_attributes(
|
|
229
|
+
self,
|
|
230
|
+
workflow_class: type[ObjectWorkflow],
|
|
231
|
+
workflow_id: str,
|
|
232
|
+
workflow_run_id: str = "",
|
|
233
|
+
data_attributes: dict[str, Any] = dict(),
|
|
234
|
+
):
|
|
235
|
+
wf_type = get_workflow_type_by_class(workflow_class)
|
|
236
|
+
data_attr_type_store = self._registry.get_data_attribute_types(wf_type)
|
|
237
|
+
for key, value in data_attributes.items():
|
|
238
|
+
if not data_attr_type_store.is_valid_name_or_prefix(key):
|
|
239
|
+
raise NotRegisteredError(f"data attribute {key} is not registered")
|
|
240
|
+
|
|
241
|
+
data_attr_type = data_attr_type_store.get_type(key)
|
|
242
|
+
if not isinstance(value, data_attr_type):
|
|
243
|
+
raise NotRegisteredError(
|
|
244
|
+
f"data attribute {key} is not registered as {type(value)}"
|
|
245
|
+
)
|
|
246
|
+
return self._unregistered_client.set_workflow_data_attributes(
|
|
247
|
+
workflow_id, workflow_run_id, data_attributes
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
def invoke_rpc(
|
|
251
|
+
self,
|
|
252
|
+
workflow_id: str,
|
|
253
|
+
rpc: Callable, # this can be a function: RPCWorkflow.rpc_method or a method: workflow_instance.rpc_method
|
|
254
|
+
input: Any = None,
|
|
255
|
+
return_type_hint: Optional[Type[T]] = None,
|
|
256
|
+
) -> Optional[T]:
|
|
257
|
+
wf_type = get_workflow_type_by_rpc_method(rpc)
|
|
258
|
+
rpc_name = rpc.__name__
|
|
259
|
+
rpc_info = self._registry.get_rpc_infos(wf_type)[rpc_name]
|
|
260
|
+
schema_options = self._registry.get_persistence_options(wf_type)
|
|
261
|
+
use_memo = schema_options.enable_caching
|
|
262
|
+
if rpc_info.bypass_caching_for_strong_consistency:
|
|
263
|
+
use_memo = False
|
|
264
|
+
|
|
265
|
+
return self._unregistered_client.invoke_rpc(
|
|
266
|
+
input=input,
|
|
267
|
+
workflow_id=workflow_id,
|
|
268
|
+
workflow_run_id="",
|
|
269
|
+
rpc_name=rpc_name,
|
|
270
|
+
timeout_seconds=rpc_info.timeout_seconds,
|
|
271
|
+
data_attribute_policy=rpc_info.data_attribute_loading_policy,
|
|
272
|
+
use_memo_for_data_attributes=use_memo,
|
|
273
|
+
all_defined_search_attribute_types=[],
|
|
274
|
+
return_type_hint=return_type_hint,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def signal_workflow(
|
|
278
|
+
self,
|
|
279
|
+
workflow_id: str,
|
|
280
|
+
signal_channel_name: str,
|
|
281
|
+
signal_value: Optional[Any] = None,
|
|
282
|
+
):
|
|
283
|
+
return self._unregistered_client.signal_workflow(
|
|
284
|
+
workflow_id, "", signal_channel_name, signal_value
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
def reset_workflow(
|
|
288
|
+
self,
|
|
289
|
+
workflow_id: str,
|
|
290
|
+
reset_workflow_type_and_options: ResetWorkflowTypeAndOptions,
|
|
291
|
+
):
|
|
292
|
+
return self._unregistered_client.reset_workflow(
|
|
293
|
+
workflow_id, "", reset_workflow_type_and_options
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
def describe_workflow(
|
|
297
|
+
self,
|
|
298
|
+
workflow_id: str,
|
|
299
|
+
workflow_run_id: Optional[str] = None,
|
|
300
|
+
):
|
|
301
|
+
run_id = workflow_run_id if workflow_run_id is not None else ""
|
|
302
|
+
|
|
303
|
+
response = self._unregistered_client.get_workflow(workflow_id, run_id)
|
|
304
|
+
return WorkflowInfo(workflow_status=response.workflow_status)
|
|
305
|
+
|
|
306
|
+
def skip_timer_by_command_id(
|
|
307
|
+
self,
|
|
308
|
+
workflow_id: str,
|
|
309
|
+
workflow_state_id: str,
|
|
310
|
+
timer_command_id: str,
|
|
311
|
+
state_execution_number: int = 1,
|
|
312
|
+
):
|
|
313
|
+
return self._unregistered_client.skip_timer_by_command_id(
|
|
314
|
+
workflow_id,
|
|
315
|
+
"",
|
|
316
|
+
workflow_state_id,
|
|
317
|
+
timer_command_id=timer_command_id,
|
|
318
|
+
state_execution_number=state_execution_number,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
def skip_timer_at_command_index(
|
|
322
|
+
self,
|
|
323
|
+
workflow_id: str,
|
|
324
|
+
workflow_state_id: Union[str, type[WorkflowState]],
|
|
325
|
+
state_execution_number: int = 1,
|
|
326
|
+
timer_command_index: int = 0,
|
|
327
|
+
):
|
|
328
|
+
if isinstance(workflow_state_id, type):
|
|
329
|
+
state_id = get_state_id_by_class(workflow_state_id)
|
|
330
|
+
else:
|
|
331
|
+
state_id = workflow_state_id
|
|
332
|
+
return self._unregistered_client.skip_timer_at_command_index(
|
|
333
|
+
workflow_id,
|
|
334
|
+
"",
|
|
335
|
+
state_id,
|
|
336
|
+
state_execution_number,
|
|
337
|
+
timer_command_index,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
def get_all_search_attributes(
|
|
341
|
+
self,
|
|
342
|
+
workflow_class: type[ObjectWorkflow],
|
|
343
|
+
workflow_id: str,
|
|
344
|
+
workflow_run_id: Optional[str] = None,
|
|
345
|
+
):
|
|
346
|
+
run_id = workflow_run_id if workflow_run_id is not None else ""
|
|
347
|
+
|
|
348
|
+
return self._do_get_workflow_search_attributes(
|
|
349
|
+
workflow_class, workflow_id, run_id
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
def get_workflow_search_attributes(
|
|
353
|
+
self,
|
|
354
|
+
workflow_class: type[ObjectWorkflow],
|
|
355
|
+
workflow_id: str,
|
|
356
|
+
attribute_keys: list[str],
|
|
357
|
+
workflow_run_id: Optional[str] = None,
|
|
358
|
+
):
|
|
359
|
+
if not attribute_keys:
|
|
360
|
+
raise ValueError(
|
|
361
|
+
"attribute_keys must contain at least one entry, or use get_all_search_attributes API to get all"
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
run_id = workflow_run_id if workflow_run_id is not None else ""
|
|
365
|
+
|
|
366
|
+
return self._do_get_workflow_search_attributes(
|
|
367
|
+
workflow_class, workflow_id, run_id, attribute_keys
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
def _do_get_workflow_search_attributes(
|
|
371
|
+
self,
|
|
372
|
+
workflow_class: type[ObjectWorkflow],
|
|
373
|
+
workflow_id: str,
|
|
374
|
+
workflow_run_id: str,
|
|
375
|
+
attribute_keys: Optional[list[str]] = None,
|
|
376
|
+
):
|
|
377
|
+
wf_type = get_workflow_type_by_class(workflow_class)
|
|
378
|
+
self._registry.get_workflow_with_check(wf_type)
|
|
379
|
+
|
|
380
|
+
search_attribute_types = self._registry.get_search_attribute_types(wf_type)
|
|
381
|
+
|
|
382
|
+
# if attribute keys is None, will fetch all registered search attributes from the server
|
|
383
|
+
if attribute_keys:
|
|
384
|
+
non_existing_search_attribute_list: list[str] = []
|
|
385
|
+
for attribute_key in attribute_keys:
|
|
386
|
+
if attribute_key not in search_attribute_types:
|
|
387
|
+
non_existing_search_attribute_list.append(attribute_key)
|
|
388
|
+
|
|
389
|
+
if non_existing_search_attribute_list:
|
|
390
|
+
raise InvalidArgumentError(
|
|
391
|
+
f"Search attributes not registered: {','.join(non_existing_search_attribute_list)}"
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
key_and_types: list[SearchAttributeKeyAndType] = []
|
|
395
|
+
if attribute_keys is None:
|
|
396
|
+
for attribute_key, sa_type in search_attribute_types.items():
|
|
397
|
+
key_and_types.append(SearchAttributeKeyAndType(attribute_key, sa_type))
|
|
398
|
+
else:
|
|
399
|
+
for attribute_key in attribute_keys:
|
|
400
|
+
sa_type = search_attribute_types[attribute_key]
|
|
401
|
+
key_and_types.append(SearchAttributeKeyAndType(attribute_key, sa_type))
|
|
402
|
+
|
|
403
|
+
response = self._unregistered_client.get_workflow_search_attributes(
|
|
404
|
+
workflow_id, workflow_run_id, key_and_types
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
response_sas = response.search_attributes
|
|
408
|
+
|
|
409
|
+
# TODO: troubleshoot why unset_to_none doesn't work as expected with lists
|
|
410
|
+
if isinstance(response_sas, Unset) or response_sas is None:
|
|
411
|
+
raise RuntimeError("search attributes not returned")
|
|
412
|
+
|
|
413
|
+
result: dict[str, Any] = {}
|
|
414
|
+
|
|
415
|
+
for response_sa in response_sas:
|
|
416
|
+
response_sa_key = unset_to_none(response_sa.key)
|
|
417
|
+
if response_sa_key is None:
|
|
418
|
+
raise RuntimeError("search attribute key is None")
|
|
419
|
+
response_sa_type = search_attribute_types[response_sa_key]
|
|
420
|
+
value = get_search_attribute_value(response_sa_type, response_sa)
|
|
421
|
+
result[response_sa_key] = value
|
|
422
|
+
|
|
423
|
+
return result
|
|
424
|
+
|
|
425
|
+
def set_workflow_search_attributes(
|
|
426
|
+
self,
|
|
427
|
+
workflow_class: type[ObjectWorkflow],
|
|
428
|
+
workflow_id: str,
|
|
429
|
+
search_attributes: list[SearchAttribute],
|
|
430
|
+
workflow_run_id: Optional[str] = None,
|
|
431
|
+
):
|
|
432
|
+
run_id = workflow_run_id if workflow_run_id is not None else ""
|
|
433
|
+
|
|
434
|
+
return self._do_set_workflow_search_attributes(
|
|
435
|
+
workflow_class, workflow_id, run_id, search_attributes
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
"""A long poll API to wait for the completion of the state.
|
|
439
|
+
Note 1 The state_completion to wait for is needed to registered on starting workflow due to limitation in https://github.com/superdurable/dex/issues/349
|
|
440
|
+
Note 2 The max polling time is configured in client_options (default to 10s)
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
state_class the state class.
|
|
444
|
+
workflow_id the workflowId
|
|
445
|
+
state_execution_number the state execution number. E.g. if it's 2, it means the 2nd execution of the state
|
|
446
|
+
"""
|
|
447
|
+
|
|
448
|
+
def wait_for_state_execution_completion_with_state_execution_id(
|
|
449
|
+
self,
|
|
450
|
+
state_class: type[WorkflowState],
|
|
451
|
+
workflow_id: str,
|
|
452
|
+
state_execution_number: int = 1,
|
|
453
|
+
):
|
|
454
|
+
state_execution_id = get_state_execution_id(state_class, state_execution_number)
|
|
455
|
+
|
|
456
|
+
self._unregistered_client.wait_for_state_execution_completion_with_state_execution_id(
|
|
457
|
+
workflow_id, state_execution_id
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
"""A long poll API to wait for the completion of the state.
|
|
461
|
+
Note 1 The state_completion to wait for is needed to registered on starting workflow due to limitation in https://github.com/superdurable/dex/issues/349
|
|
462
|
+
Note 2 The max polling time is configured in client_options (default to 10s)
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
state_class the state class.
|
|
466
|
+
workflow_id the workflowId
|
|
467
|
+
wait_for_key key provided by the client and to identity workflow
|
|
468
|
+
"""
|
|
469
|
+
|
|
470
|
+
def wait_for_state_execution_completion_with_wait_for_key(
|
|
471
|
+
self, state_class: type[WorkflowState], workflow_id: str, wait_for_key: str
|
|
472
|
+
):
|
|
473
|
+
state_id = get_state_id_by_class(state_class)
|
|
474
|
+
|
|
475
|
+
self._unregistered_client.wait_for_state_execution_completion_with_wait_for_key(
|
|
476
|
+
workflow_id, state_id, wait_for_key
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
def _do_set_workflow_search_attributes(
|
|
480
|
+
self,
|
|
481
|
+
workflow_class: type[ObjectWorkflow],
|
|
482
|
+
workflow_id: str,
|
|
483
|
+
workflow_run_id: str,
|
|
484
|
+
search_attributes: list[SearchAttribute],
|
|
485
|
+
):
|
|
486
|
+
wf_type = get_workflow_type_by_class(workflow_class)
|
|
487
|
+
self._registry.get_workflow_with_check(wf_type)
|
|
488
|
+
|
|
489
|
+
search_attribute_types = self._registry.get_search_attribute_types(wf_type)
|
|
490
|
+
|
|
491
|
+
# Check that the requested sa type is registered to the key
|
|
492
|
+
for search_attribute in search_attributes:
|
|
493
|
+
sa_key = unset_to_none(search_attribute.key)
|
|
494
|
+
if sa_key is None:
|
|
495
|
+
raise RuntimeError("search attribute key is None")
|
|
496
|
+
if sa_key not in search_attribute_types:
|
|
497
|
+
raise InvalidArgumentError(f"Search attribute not registered: {sa_key}")
|
|
498
|
+
registered_value_type = search_attribute_types[sa_key]
|
|
499
|
+
|
|
500
|
+
sa_value_type = unset_to_none(search_attribute.value_type)
|
|
501
|
+
if sa_value_type is None:
|
|
502
|
+
raise RuntimeError("search value type is None")
|
|
503
|
+
|
|
504
|
+
if (
|
|
505
|
+
sa_value_type is not None
|
|
506
|
+
and registered_value_type != sa_value_type.value
|
|
507
|
+
):
|
|
508
|
+
raise ValueError(
|
|
509
|
+
f"Search attribute key, {sa_key} is registered to type {registered_value_type}, but tried to add search attribute type {sa_value_type.value}"
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
self._unregistered_client.set_workflow_search_attributes(
|
|
513
|
+
workflow_id, workflow_run_id, search_attributes
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def convert_to_sa_list(
|
|
518
|
+
sa_types: dict[str, SearchAttributeValueType], initial_sas: dict[str, Any]
|
|
519
|
+
):
|
|
520
|
+
converted_sas: list[SearchAttribute] = []
|
|
521
|
+
if initial_sas:
|
|
522
|
+
for initial_sa_key, initial_sa_val in initial_sas.items():
|
|
523
|
+
if initial_sa_key not in sa_types:
|
|
524
|
+
raise WorkflowDefinitionError(
|
|
525
|
+
f"key {initial_sa_key} is not defined as search attribute, all keys are: {','.join(sa_types)}"
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
val_type = sa_types[initial_sa_key]
|
|
529
|
+
new_sa = SearchAttribute(key=initial_sa_key, value_type=val_type)
|
|
530
|
+
is_val_correct_type = False
|
|
531
|
+
if val_type == SearchAttributeValueType.INT:
|
|
532
|
+
if isinstance(initial_sa_val, int):
|
|
533
|
+
new_sa.integer_value = initial_sa_val
|
|
534
|
+
converted_sas.append(new_sa)
|
|
535
|
+
is_val_correct_type = True
|
|
536
|
+
elif val_type == SearchAttributeValueType.DOUBLE:
|
|
537
|
+
if isinstance(initial_sa_val, float):
|
|
538
|
+
new_sa.double_value = initial_sa_val
|
|
539
|
+
converted_sas.append(new_sa)
|
|
540
|
+
is_val_correct_type = True
|
|
541
|
+
elif val_type == SearchAttributeValueType.BOOL:
|
|
542
|
+
if isinstance(initial_sa_val, bool):
|
|
543
|
+
new_sa.bool_value = initial_sa_val
|
|
544
|
+
converted_sas.append(new_sa)
|
|
545
|
+
is_val_correct_type = True
|
|
546
|
+
elif (
|
|
547
|
+
val_type == SearchAttributeValueType.KEYWORD
|
|
548
|
+
or val_type == SearchAttributeValueType.TEXT
|
|
549
|
+
or val_type == SearchAttributeValueType.DATETIME
|
|
550
|
+
):
|
|
551
|
+
if isinstance(initial_sa_val, str):
|
|
552
|
+
new_sa.string_value = initial_sa_val
|
|
553
|
+
converted_sas.append(new_sa)
|
|
554
|
+
is_val_correct_type = True
|
|
555
|
+
elif val_type == SearchAttributeValueType.KEYWORD_ARRAY:
|
|
556
|
+
if isinstance(initial_sa_val, list):
|
|
557
|
+
new_sa.string_array_value = initial_sa_val
|
|
558
|
+
converted_sas.append(new_sa)
|
|
559
|
+
is_val_correct_type = True
|
|
560
|
+
else:
|
|
561
|
+
raise ValueError("unsupported type")
|
|
562
|
+
|
|
563
|
+
if not is_val_correct_type:
|
|
564
|
+
raise InvalidArgumentError(
|
|
565
|
+
f"search attribute value is not set correctly for key {initial_sa_key} with value type {val_type}"
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
return converted_sas
|
dex/client_options.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Copyright (c) 2022-2026 Super Durable, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
|
|
17
|
+
from dex.object_encoder import ObjectEncoder
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ClientOptions:
|
|
22
|
+
server_url: str
|
|
23
|
+
worker_url: str
|
|
24
|
+
object_encoder: ObjectEncoder
|
|
25
|
+
api_timeout: int = 60
|
|
26
|
+
long_poll_api_max_wait_time_seconds: int = 10
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def local_default(cls):
|
|
30
|
+
return ClientOptions(
|
|
31
|
+
server_url="http://localhost:8801",
|
|
32
|
+
worker_url="http://localhost:8802",
|
|
33
|
+
object_encoder=ObjectEncoder.default,
|
|
34
|
+
)
|