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/worker_service.py ADDED
@@ -0,0 +1,441 @@
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 traceback
16
+ import typing
17
+ from dataclasses import dataclass
18
+ from typing import List, Union
19
+
20
+ from dex.command_request import _to_idl_command_request
21
+ from dex.command_results import from_idl_command_results
22
+ from dex.communication import Communication
23
+ from dex.data_attributes import DataAttributes
24
+ from dex.dex_api.models import (
25
+ EncodedObject,
26
+ KeyValue,
27
+ SearchAttribute,
28
+ SearchAttributeValueType,
29
+ WorkflowStateExecuteRequest,
30
+ WorkflowStateExecuteResponse,
31
+ WorkflowStateWaitUntilRequest,
32
+ WorkflowStateWaitUntilResponse,
33
+ WorkflowWorkerRpcRequest,
34
+ WorkflowWorkerRpcResponse,
35
+ )
36
+ from dex.dex_api.types import Unset
37
+ from dex.object_encoder import ObjectEncoder
38
+ from dex.persistence import Persistence
39
+ from dex.registry import Registry
40
+ from dex.search_attributes import SearchAttributes
41
+ from dex.state_decision import StateDecision, _to_idl_state_decision
42
+ from dex.state_execution_locals import StateExecutionLocals
43
+ from dex.utils.dex_typing import assert_not_unset, unset_to_none
44
+ from dex.workflow_context import WorkflowContext, _from_idl_context
45
+ from dex.workflow_state import get_input_type
46
+
47
+
48
+ @dataclass
49
+ class WorkerOptions:
50
+ object_encoder: ObjectEncoder
51
+
52
+
53
+ default_worker_options = WorkerOptions(ObjectEncoder.default)
54
+
55
+
56
+ class WorkerService:
57
+ api_path_workflow_state_wait_until: typing.ClassVar[str] = (
58
+ "/api/v1/workflowState/start"
59
+ )
60
+ api_path_workflow_state_execute: typing.ClassVar[str] = (
61
+ "/api/v1/workflowState/decide"
62
+ )
63
+ api_path_workflow_worker_rpc: typing.ClassVar[str] = "/api/v1/workflowWorker/rpc"
64
+
65
+ def __init__(
66
+ self, registry: Registry, options: WorkerOptions = default_worker_options
67
+ ):
68
+ self._registry = registry
69
+ self._options = options
70
+
71
+ @staticmethod
72
+ def handle_worker_error(exception: Exception):
73
+ """
74
+ Handle the exception/error of worker so that Temporal/Cadence WebUI can show the error nicely.
75
+ Example usage (in Flask):
76
+ @_flask_app.errorhandler(Exception)
77
+ def internal_error(exception):
78
+ return _worker_service.handle_worker_error(exception), 500
79
+ """
80
+ stacktrace = traceback.format_exc()
81
+ index = max(0, stacktrace.find("dex-python-sdk/dex/worker_service.py"))
82
+ return "WorkerExecutionError: {0}; StackTrace:{1}".format(
83
+ exception, stacktrace[index:]
84
+ )
85
+
86
+ def handle_workflow_worker_rpc(
87
+ self,
88
+ request: WorkflowWorkerRpcRequest,
89
+ ) -> WorkflowWorkerRpcResponse:
90
+ wf_type = request.workflow_type
91
+ rpc_info = self._registry.get_rpc_infos(wf_type)[request.rpc_name]
92
+
93
+ internal_channel_types = self._registry.get_internal_channel_type_store(wf_type)
94
+ signal_channel_types = self._registry.get_signal_channel_types(wf_type)
95
+ data_attributes_types = self._registry.get_data_attribute_types(wf_type)
96
+
97
+ context = _from_idl_context(request.context)
98
+ _input = self._options.object_encoder.decode(
99
+ unset_to_none(request.input_), rpc_info.input_type
100
+ )
101
+
102
+ current_data_attributes: dict[str, typing.Union[EncodedObject, None, Unset]] = (
103
+ {}
104
+ )
105
+ if not isinstance(request.data_attributes, Unset):
106
+ current_data_attributes = {
107
+ assert_not_unset(attr.key): unset_to_none(attr.value)
108
+ for attr in request.data_attributes
109
+ }
110
+
111
+ data_attributes = DataAttributes(
112
+ data_attributes_types,
113
+ self._options.object_encoder,
114
+ current_data_attributes,
115
+ )
116
+
117
+ search_attributes_types = self._registry.get_search_attribute_types(wf_type)
118
+ search_attributes = SearchAttributes(
119
+ search_attributes_types, unset_to_none(request.search_attributes)
120
+ )
121
+ state_execution_locals = StateExecutionLocals(
122
+ to_map(None), self._options.object_encoder
123
+ )
124
+
125
+ persistence = Persistence(
126
+ data_attributes, search_attributes, state_execution_locals
127
+ )
128
+
129
+ communication = Communication(
130
+ internal_channel_types,
131
+ signal_channel_types,
132
+ self._options.object_encoder,
133
+ unset_to_none(request.internal_channel_infos),
134
+ unset_to_none(request.signal_channel_infos),
135
+ )
136
+ params: typing.Any = []
137
+ if rpc_info.params_order is not None:
138
+ for param_type in rpc_info.params_order:
139
+ if param_type == Persistence:
140
+ params.append(persistence)
141
+ elif param_type == Communication:
142
+ params.append(communication)
143
+ elif param_type == WorkflowContext:
144
+ params.append(context)
145
+ else:
146
+ params.append(_input)
147
+
148
+ output = rpc_info.method_func(*params)
149
+
150
+ pubs = communication.get_to_publishing_internal_channel()
151
+ response = WorkflowWorkerRpcResponse(
152
+ output=self._options.object_encoder.encode(output)
153
+ )
154
+
155
+ if len(pubs) > 0:
156
+ response.publish_to_inter_state_channel = pubs
157
+ if len(data_attributes.get_updated_values_to_return()) > 0:
158
+ response.upsert_data_attributes = [
159
+ KeyValue(k, v)
160
+ for (k, v) in data_attributes.get_updated_values_to_return().items()
161
+ ]
162
+ upsert_sas = _create_upsert_search_attributes(
163
+ search_attributes_types,
164
+ search_attributes.get_upsert_to_server_int64_attribute_map(),
165
+ search_attributes.get_upsert_to_server_string_attribute_map(),
166
+ search_attributes.get_upsert_to_server_bool_attribute_map(),
167
+ search_attributes.get_upsert_to_server_double_attribute_map(),
168
+ search_attributes.get_upsert_to_server_string_array_attribute_map(),
169
+ )
170
+ if upsert_sas:
171
+ response.upsert_search_attributes = upsert_sas
172
+ record_events = state_execution_locals.get_record_events()
173
+ if len(record_events) > 0:
174
+ response.record_events = record_events
175
+ if len(communication.get_to_trigger_state_movements()) > 0:
176
+ movements = communication.get_to_trigger_state_movements()
177
+ decision = StateDecision.multi_next_states(*movements)
178
+ response.state_decision = _to_idl_state_decision(
179
+ decision,
180
+ wf_type,
181
+ self._registry,
182
+ self._options.object_encoder,
183
+ )
184
+ return response
185
+
186
+ def handle_workflow_state_wait_until(
187
+ self,
188
+ request: WorkflowStateWaitUntilRequest,
189
+ ) -> WorkflowStateWaitUntilResponse:
190
+ wf_type = request.workflow_type
191
+ state = self._registry.get_workflow_state_with_check(
192
+ wf_type, request.workflow_state_id
193
+ )
194
+ internal_channel_types = self._registry.get_internal_channel_type_store(wf_type)
195
+ signal_channel_types = self._registry.get_signal_channel_types(wf_type)
196
+ data_attributes_types = self._registry.get_data_attribute_types(wf_type)
197
+
198
+ context = _from_idl_context(request.context)
199
+ _input = self._options.object_encoder.decode(
200
+ unset_to_none(request.state_input), get_input_type(state)
201
+ )
202
+
203
+ current_data_attributes: dict[str, typing.Union[EncodedObject, None, Unset]] = (
204
+ {}
205
+ )
206
+ if not isinstance(request.data_objects, Unset):
207
+ current_data_attributes = {
208
+ assert_not_unset(attr.key): unset_to_none(attr.value)
209
+ for attr in request.data_objects
210
+ }
211
+
212
+ data_attributes = DataAttributes(
213
+ data_attributes_types,
214
+ self._options.object_encoder,
215
+ current_data_attributes,
216
+ )
217
+
218
+ search_attributes_types = self._registry.get_search_attribute_types(wf_type)
219
+ search_attributes = SearchAttributes(
220
+ search_attributes_types, unset_to_none(request.search_attributes)
221
+ )
222
+ state_execution_locals = StateExecutionLocals(
223
+ to_map(None), self._options.object_encoder
224
+ )
225
+
226
+ persistence = Persistence(
227
+ data_attributes, search_attributes, state_execution_locals
228
+ )
229
+
230
+ communication = Communication(
231
+ internal_channel_types,
232
+ signal_channel_types,
233
+ self._options.object_encoder,
234
+ None,
235
+ None,
236
+ )
237
+ command_request = state.wait_until(context, _input, persistence, communication)
238
+
239
+ pubs = communication.get_to_publishing_internal_channel()
240
+
241
+ upsert_sas = _create_upsert_search_attributes(
242
+ search_attributes_types,
243
+ search_attributes.get_upsert_to_server_int64_attribute_map(),
244
+ search_attributes.get_upsert_to_server_string_attribute_map(),
245
+ search_attributes.get_upsert_to_server_bool_attribute_map(),
246
+ search_attributes.get_upsert_to_server_double_attribute_map(),
247
+ search_attributes.get_upsert_to_server_string_array_attribute_map(),
248
+ )
249
+
250
+ upsert_state_locals = (
251
+ state_execution_locals.get_upsert_state_execution_local_attributes()
252
+ )
253
+ record_events = state_execution_locals.get_record_events()
254
+
255
+ response = WorkflowStateWaitUntilResponse(
256
+ command_request=_to_idl_command_request(command_request),
257
+ publish_to_inter_state_channel=pubs,
258
+ upsert_data_objects=[
259
+ KeyValue(k, v)
260
+ for (k, v) in data_attributes.get_updated_values_to_return().items()
261
+ ],
262
+ upsert_state_locals=upsert_state_locals,
263
+ record_events=record_events,
264
+ )
265
+
266
+ if upsert_sas:
267
+ response.upsert_search_attributes = upsert_sas
268
+
269
+ return response
270
+
271
+ def handle_workflow_state_execute(
272
+ self,
273
+ request: WorkflowStateExecuteRequest,
274
+ ) -> WorkflowStateExecuteResponse:
275
+ wf_type = request.workflow_type
276
+ state = self._registry.get_workflow_state_with_check(
277
+ wf_type, request.workflow_state_id
278
+ )
279
+ internal_channel_types = self._registry.get_internal_channel_type_store(wf_type)
280
+ signal_channel_types = self._registry.get_signal_channel_types(wf_type)
281
+ data_attributes_types = self._registry.get_data_attribute_types(wf_type)
282
+ context = _from_idl_context(request.context)
283
+
284
+ _input = self._options.object_encoder.decode(
285
+ unset_to_none(request.state_input), get_input_type(state)
286
+ )
287
+
288
+ current_data_attributes: dict[str, typing.Union[EncodedObject, None, Unset]] = (
289
+ {}
290
+ )
291
+ if not isinstance(request.data_objects, Unset):
292
+ current_data_attributes = {
293
+ assert_not_unset(attr.key): unset_to_none(attr.value)
294
+ for attr in request.data_objects
295
+ }
296
+
297
+ data_attributes = DataAttributes(
298
+ data_attributes_types,
299
+ self._options.object_encoder,
300
+ current_data_attributes,
301
+ )
302
+
303
+ search_attributes_types = self._registry.get_search_attribute_types(wf_type)
304
+ search_attributes = SearchAttributes(
305
+ search_attributes_types, unset_to_none(request.search_attributes)
306
+ )
307
+ state_execution_locals = StateExecutionLocals(
308
+ to_map(request.state_locals), self._options.object_encoder
309
+ )
310
+
311
+ persistence = Persistence(
312
+ data_attributes, search_attributes, state_execution_locals
313
+ )
314
+
315
+ communication = Communication(
316
+ internal_channel_types,
317
+ signal_channel_types,
318
+ self._options.object_encoder,
319
+ None,
320
+ None,
321
+ )
322
+
323
+ command_results = from_idl_command_results(
324
+ request.command_results,
325
+ internal_channel_types,
326
+ signal_channel_types,
327
+ self._options.object_encoder,
328
+ )
329
+ decision = state.execute(
330
+ context, _input, command_results, persistence, communication
331
+ )
332
+
333
+ pubs = communication.get_to_publishing_internal_channel()
334
+
335
+ upsert_sas = _create_upsert_search_attributes(
336
+ search_attributes_types,
337
+ search_attributes.get_upsert_to_server_int64_attribute_map(),
338
+ search_attributes.get_upsert_to_server_string_attribute_map(),
339
+ search_attributes.get_upsert_to_server_bool_attribute_map(),
340
+ search_attributes.get_upsert_to_server_double_attribute_map(),
341
+ search_attributes.get_upsert_to_server_string_array_attribute_map(),
342
+ )
343
+ upsert_state_locals = (
344
+ state_execution_locals.get_upsert_state_execution_local_attributes()
345
+ )
346
+ record_events = state_execution_locals.get_record_events()
347
+
348
+ response = WorkflowStateExecuteResponse(
349
+ state_decision=_to_idl_state_decision(
350
+ decision,
351
+ wf_type,
352
+ self._registry,
353
+ self._options.object_encoder,
354
+ ),
355
+ publish_to_inter_state_channel=pubs,
356
+ upsert_data_objects=[
357
+ KeyValue(k, v)
358
+ for (k, v) in data_attributes.get_updated_values_to_return().items()
359
+ ],
360
+ upsert_state_locals=upsert_state_locals,
361
+ record_events=record_events,
362
+ )
363
+
364
+ if upsert_sas:
365
+ response.upsert_search_attributes = upsert_sas
366
+
367
+ return response
368
+
369
+
370
+ def _create_upsert_search_attributes(
371
+ type_map: dict[str, SearchAttributeValueType],
372
+ upsert_to_server_int64_attribute_map: dict[str, Union[int, None]],
373
+ upsert_to_server_keyword_attribute_map: dict[str, Union[str, None]],
374
+ upsert_to_server_bool_attribute_map: dict[str, Union[bool, None]],
375
+ upsert_to_server_double_attribute_map: dict[str, Union[float, None]],
376
+ upsert_to_server_string_array_attribute_map: dict[str, Union[list[str], None]],
377
+ ):
378
+ sas: list[SearchAttribute] = []
379
+ for int_key, int_sa in upsert_to_server_int64_attribute_map.items():
380
+ sa = SearchAttribute(
381
+ key=int_key,
382
+ value_type=type_map[int_key],
383
+ )
384
+ if int_sa is not None:
385
+ sa.integer_value = int_sa
386
+ sas.append(sa)
387
+
388
+ for keyword_key, keyword_sa in upsert_to_server_keyword_attribute_map.items():
389
+ sa = SearchAttribute(
390
+ key=keyword_key,
391
+ value_type=type_map[keyword_key],
392
+ )
393
+ if keyword_sa is not None:
394
+ sa.string_value = keyword_sa
395
+ sas.append(sa)
396
+
397
+ for bool_key, bool_sa in upsert_to_server_bool_attribute_map.items():
398
+ sa = SearchAttribute(
399
+ key=bool_key,
400
+ value_type=type_map[bool_key],
401
+ )
402
+ if bool_sa is not None:
403
+ sa.bool_value = bool_sa
404
+ sas.append(sa)
405
+
406
+ for double_key, double_sa in upsert_to_server_double_attribute_map.items():
407
+ sa = SearchAttribute(
408
+ key=double_key,
409
+ value_type=type_map[double_key],
410
+ )
411
+ if double_sa is not None:
412
+ sa.double_value = double_sa
413
+ sas.append(sa)
414
+
415
+ for (
416
+ string_array_key,
417
+ string_array_sa,
418
+ ) in upsert_to_server_string_array_attribute_map.items():
419
+ sa = SearchAttribute(
420
+ key=string_array_key,
421
+ value_type=type_map[string_array_key],
422
+ )
423
+ if string_array_sa is not None:
424
+ sa.string_array_value = string_array_sa
425
+ sas.append(sa)
426
+
427
+ return sas
428
+
429
+
430
+ def to_map(
431
+ key_values: Union[None, Unset, List[KeyValue]],
432
+ ) -> dict[str, Union[EncodedObject, Unset]]:
433
+ key_values = unset_to_none(key_values) or []
434
+ kvs: dict[str, Union[EncodedObject, Unset]] = {}
435
+ for kv in key_values:
436
+ k = unset_to_none(kv.key)
437
+ v = unset_to_none(kv.value)
438
+ if k and v:
439
+ kvs[k] = v
440
+
441
+ return kvs
dex/workflow.py ADDED
@@ -0,0 +1,86 @@
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 abc import ABC
16
+
17
+ from dex.communication_schema import CommunicationSchema
18
+ from dex.persistence_options import PersistenceOptions
19
+ from dex.persistence_schema import PersistenceSchema
20
+ from dex.state_schema import StateSchema
21
+
22
+
23
+ class ObjectWorkflow(ABC):
24
+ """ObjectWorkflow is the interface to define a workflow definition.
25
+ ObjectWorkflow is a top level concept in Dex. Any object that is long-lasting
26
+ can be modeled as an ObjectWorkflow.
27
+ """
28
+
29
+ def get_workflow_states(self) -> StateSchema:
30
+ """
31
+ GetWorkflowStates defines the states of the workflow. A state represents
32
+ a step of the workflow state machine.
33
+ A state can execute some commands (signal/timer) and wait for result
34
+ See more details in the WorkflowState interface.
35
+ It can return an empty list, meaning no states.
36
+ There can be at most one startingState in the list.
37
+ If there is no startingState or with the default empty state list, the workflow
38
+ will not start any state execution after workflow stated. Application can still
39
+ use RPC to invoke new state execution in the future.
40
+ Returns:
41
+ A list of workflow state definitions. Default to empty.
42
+ """
43
+ return StateSchema()
44
+
45
+ def get_persistence_schema(self) -> PersistenceSchema:
46
+ """
47
+ GetPersistenceSchema defines all the persistence fields for this workflow, includes:
48
+ 1. Data attributes
49
+ 2. Search attributes
50
+ Data attributes can be read/upsert in WorkflowState WaitUntil/Execute API
51
+ Data attributes can also be read by getDataAttributes API by external applications using Client
52
+ Search attributes can be read/upsert in WorkflowState WaitUntil/Execute API
53
+ Search attributes can also be read by GetSearchAttributes Client API by external applications.
54
+ External applications can also use "SearchWorkflow" API to find workflows by SQL-like query
55
+
56
+ Returns:
57
+ A persistence schema. Default to empty.
58
+ """
59
+ return PersistenceSchema()
60
+
61
+ def get_persistence_options(self) -> PersistenceOptions:
62
+ return PersistenceOptions.get_default()
63
+
64
+ def get_communication_schema(self) -> CommunicationSchema:
65
+ """
66
+ GetCommunicationSchema defines all the communication methods for this workflow, this includes
67
+ 1. Signal channel
68
+ 2. Interstate channel
69
+ Signal channel is for external applications to send signal to workflow execution.
70
+ ObjectWorkflow execution can listen on the signal in the WorkflowState WaitUntil API and receive in
71
+ the WorkflowState Execute API
72
+ InterStateChannel is for synchronization communications between WorkflowStates.
73
+ E.g. WorkflowStateA will continue after receiving a value from WorkflowStateB
74
+
75
+ Returns:
76
+ A communication schema. Default to empty.
77
+ """
78
+ return CommunicationSchema()
79
+
80
+
81
+ def get_workflow_type(wf: ObjectWorkflow) -> str:
82
+ return wf.__class__.__name__
83
+
84
+
85
+ def get_workflow_type_by_class(wf_class: type[ObjectWorkflow]) -> str:
86
+ return wf_class.__name__
@@ -0,0 +1,50 @@
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
+ from typing import Optional
17
+
18
+ from dex.dex_api.models.context import Context
19
+ from dex.utils.dex_typing import unset_to_none
20
+
21
+
22
+ @dataclass
23
+ class WorkflowContext:
24
+ workflow_id: str
25
+ workflow_run_id: str
26
+ workflow_start_timestamp_seconds: int
27
+ state_execution_id: Optional[str] = None
28
+ first_attempt_timestamp_seconds: Optional[int] = None
29
+ attempt: Optional[int] = None
30
+ child_workflow_request_id: Optional[str] = None
31
+
32
+
33
+ def _from_idl_context(idl_context: Context) -> WorkflowContext:
34
+ state_execution_id = unset_to_none(idl_context.state_execution_id)
35
+
36
+ return WorkflowContext(
37
+ workflow_id=idl_context.workflow_id,
38
+ workflow_run_id=idl_context.workflow_run_id,
39
+ workflow_start_timestamp_seconds=idl_context.workflow_started_timestamp,
40
+ state_execution_id=state_execution_id,
41
+ first_attempt_timestamp_seconds=unset_to_none(
42
+ idl_context.first_attempt_timestamp,
43
+ ),
44
+ attempt=unset_to_none(idl_context.attempt),
45
+ child_workflow_request_id=(
46
+ idl_context.workflow_run_id + "-" + state_execution_id
47
+ if state_execution_id is not None
48
+ else None
49
+ ),
50
+ )
dex/workflow_info.py ADDED
@@ -0,0 +1,21 @@
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
+ from dex.dex_api.models.workflow_status import WorkflowStatus
17
+
18
+
19
+ @dataclass
20
+ class WorkflowInfo:
21
+ workflow_status: WorkflowStatus
@@ -0,0 +1,79 @@
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, field
16
+ from typing import Any, Optional
17
+
18
+ from dex.dex_api.models import (
19
+ IDReusePolicy,
20
+ WorkflowRetryPolicy,
21
+ WorkflowAlreadyStartedOptions,
22
+ WorkflowConfig,
23
+ )
24
+ from dex.workflow_state import (
25
+ WorkflowState,
26
+ get_state_id_by_class,
27
+ get_state_execution_id,
28
+ )
29
+
30
+
31
+ @dataclass
32
+ class WorkflowOptions:
33
+ workflow_id_reuse_policy: Optional[IDReusePolicy] = None
34
+ workflow_cron_schedule: Optional[str] = None
35
+ workflow_start_delay_seconds: Optional[int] = None
36
+ workflow_retry_policy: Optional[WorkflowRetryPolicy] = None
37
+ workflow_already_started_options: Optional[WorkflowAlreadyStartedOptions] = None
38
+ workflow_config_override: Optional[WorkflowConfig] = None
39
+ initial_data_attributes: Optional[dict[str, Any]] = None
40
+ _wait_for_completion_state_ids: list[str] = field(default_factory=list)
41
+ _wait_for_completion_state_execution_ids: list[str] = field(default_factory=list)
42
+ initial_search_attributes: Optional[dict[str, Any]] = None
43
+
44
+ @property
45
+ def wait_for_completion_state_ids(self) -> Optional[list[str]]:
46
+ return self._wait_for_completion_state_ids
47
+
48
+ @wait_for_completion_state_ids.setter
49
+ def wait_for_completion_state_ids(self, *states: type[WorkflowState]):
50
+ state_ids: list[str] = []
51
+ for state in states:
52
+ state_ids.append(get_state_id_by_class(state))
53
+ self._wait_for_completion_state_ids = state_ids
54
+
55
+ def add_wait_for_completion_state_ids(self, *states: type[WorkflowState]):
56
+ for state in states:
57
+ self._wait_for_completion_state_ids.append(get_state_id_by_class(state))
58
+
59
+ @property
60
+ def wait_for_completion_state_execution_ids(self) -> Optional[list[str]]:
61
+ return self._wait_for_completion_state_execution_ids
62
+
63
+ @wait_for_completion_state_execution_ids.setter
64
+ def wait_for_completion_state_execution_ids(self, val):
65
+ try:
66
+ state, number = val
67
+ except ValueError:
68
+ raise ValueError(
69
+ "Pass an iterable with two items: state: type[WorkflowState] and number: int"
70
+ )
71
+ else:
72
+ state_id = get_state_execution_id(state, number)
73
+ self._wait_for_completion_state_execution_ids = state_id
74
+
75
+ def add_wait_for_completion_state_execution_id(
76
+ self, state: type[WorkflowState], number: int
77
+ ):
78
+ state_id = get_state_execution_id(state, number)
79
+ self._wait_for_completion_state_execution_ids.append(state_id)