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.
Files changed (93) hide show
  1. dex/__init__.py +14 -0
  2. dex/client.py +568 -0
  3. dex/client_options.py +34 -0
  4. dex/command_request.py +130 -0
  5. dex/command_results.py +116 -0
  6. dex/communication.py +141 -0
  7. dex/communication_schema.py +68 -0
  8. dex/data_attributes.py +75 -0
  9. dex/dexpb/__init__.py +1 -0
  10. dex/dexpb/dex_pb2.py +305 -0
  11. dex/dexpb/dex_pb2.pyi +1273 -0
  12. dex/dexpb/dex_pb2_grpc.py +1126 -0
  13. dex/errors.py +132 -0
  14. dex/object_encoder.py +818 -0
  15. dex/persistence.py +94 -0
  16. dex/persistence_options.py +24 -0
  17. dex/persistence_schema.py +58 -0
  18. dex/py.typed +1 -0
  19. dex/registry.py +209 -0
  20. dex/reset_workflow_type_and_options.py +77 -0
  21. dex/rpc.py +100 -0
  22. dex/search_attributes.py +189 -0
  23. dex/state_decision.py +161 -0
  24. dex/state_execution_locals.py +71 -0
  25. dex/state_movement.py +123 -0
  26. dex/state_schema.py +54 -0
  27. dex/stop_workflow_options.py +23 -0
  28. dex/tests/__init__.py +84 -0
  29. dex/tests/dex-service-env/.env +7 -0
  30. dex/tests/dex-service-env/docker-compose-init.sh +44 -0
  31. dex/tests/dex-service-env/docker-compose.yml +87 -0
  32. dex/tests/dex-service-env/dynamicconfig/README.md +39 -0
  33. dex/tests/dex-service-env/dynamicconfig/development-sql.yaml +6 -0
  34. dex/tests/dex-service-env/dynamicconfig/docker.yaml +0 -0
  35. dex/tests/test_abnormal_exit_workflow.py +48 -0
  36. dex/tests/test_basic_workflow.py +75 -0
  37. dex/tests/test_conditional_complete.py +56 -0
  38. dex/tests/test_describe_workflow.py +45 -0
  39. dex/tests/test_empty_data_decodes_properly.py +79 -0
  40. dex/tests/test_internal_channel.py +33 -0
  41. dex/tests/test_internal_channel_with_no_prefix_channel.py +46 -0
  42. dex/tests/test_persistence_data_attributes.py +67 -0
  43. dex/tests/test_persistence_search_attributes.py +132 -0
  44. dex/tests/test_persistence_state_execution_locals.py +43 -0
  45. dex/tests/test_rpc.py +69 -0
  46. dex/tests/test_rpc_with_memo.py +200 -0
  47. dex/tests/test_rpc_with_memo_duplicate_java_tests.py +122 -0
  48. dex/tests/test_signal.py +56 -0
  49. dex/tests/test_skip_wait_until.py +85 -0
  50. dex/tests/test_state_failure_recovery.py +33 -0
  51. dex/tests/test_timer.py +40 -0
  52. dex/tests/test_wait_for_state_execution_completion.py +58 -0
  53. dex/tests/test_workflow_errors.py +92 -0
  54. dex/tests/test_workflow_state_options.py +123 -0
  55. dex/tests/test_workflow_state_options_override.py +49 -0
  56. dex/tests/worker_server.py +74 -0
  57. dex/tests/workflows/abnormal_exit_workflow.py +48 -0
  58. dex/tests/workflows/basic_workflow.py +69 -0
  59. dex/tests/workflows/conditional_complete_workflow.py +101 -0
  60. dex/tests/workflows/describe_workflow.py +52 -0
  61. dex/tests/workflows/empty_data_workflow.py +51 -0
  62. dex/tests/workflows/internal_channel_workflow.py +137 -0
  63. dex/tests/workflows/internal_channel_workflow_with_no_prefix_channel.py +108 -0
  64. dex/tests/workflows/java_duplicate_rpc_memo_workflow.py +285 -0
  65. dex/tests/workflows/persistence_data_attributes_workflow.py +104 -0
  66. dex/tests/workflows/persistence_search_attributes_workflow.py +167 -0
  67. dex/tests/workflows/persistence_state_execution_local_workflow.py +69 -0
  68. dex/tests/workflows/recovery_workflow.py +90 -0
  69. dex/tests/workflows/rpc_memo_workflow.py +238 -0
  70. dex/tests/workflows/rpc_workflow.py +125 -0
  71. dex/tests/workflows/state_options_override_workflow.py +100 -0
  72. dex/tests/workflows/state_options_workflow.py +92 -0
  73. dex/tests/workflows/timer_workflow.py +52 -0
  74. dex/tests/workflows/wait_for_state_with_state_execution_id_workflow.py +77 -0
  75. dex/tests/workflows/wait_for_state_with_wait_for_key_workflow.py +78 -0
  76. dex/tests/workflows/wait_internal_channel_workflow.py +53 -0
  77. dex/tests/workflows/wait_signal_workflow.py +154 -0
  78. dex/type_store.py +105 -0
  79. dex/unregistered_client.py +595 -0
  80. dex/utils/__init__.py +14 -0
  81. dex/utils/dex_typing.py +31 -0
  82. dex/utils/persistence_utils.py +37 -0
  83. dex/worker_service.py +441 -0
  84. dex/workflow.py +86 -0
  85. dex/workflow_context.py +50 -0
  86. dex/workflow_info.py +21 -0
  87. dex/workflow_options.py +79 -0
  88. dex/workflow_state.py +134 -0
  89. dex/workflow_state_options.py +160 -0
  90. dex_python_sdk-0.0.1.dist-info/METADATA +140 -0
  91. dex_python_sdk-0.0.1.dist-info/RECORD +93 -0
  92. dex_python_sdk-0.0.1.dist-info/WHEEL +4 -0
  93. 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
+ )