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
@@ -0,0 +1,189 @@
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 typing import Union
16
+
17
+ from dex.errors import WorkflowDefinitionError
18
+ from dex.dex_api.models import SearchAttribute, SearchAttributeValueType
19
+ from dex.utils.dex_typing import unset_to_none
20
+
21
+
22
+ class SearchAttributes:
23
+ _key_to_type_map: dict[str, SearchAttributeValueType]
24
+ _int64_attribute_map: dict[str, Union[int, None]]
25
+ _upsert_to_server_int64_attribute_map: dict[str, Union[int, None]]
26
+ _string_attribute_map: dict[str, Union[str, None]]
27
+ _upsert_to_server_string_attribute_map: dict[str, Union[str, None]]
28
+ _double_attribute_map: dict[str, Union[float, None]]
29
+ _upsert_to_server_double_attribute_map: dict[str, Union[float, None]]
30
+ _bool_attribute_map: dict[str, Union[bool, None]]
31
+ _upsert_to_server_bool_attribute_map: dict[str, Union[bool, None]]
32
+ _string_array_attribute_map: dict[str, Union[list[str], None]]
33
+ _upsert_to_server_string_array_attribute_map: dict[str, Union[list[str], None]]
34
+
35
+ def __init__(
36
+ self,
37
+ key_to_type_map: dict[str, SearchAttributeValueType],
38
+ search_attribute_map: Union[list[SearchAttribute], None],
39
+ ):
40
+ self._key_to_type_map = key_to_type_map
41
+ self._int64_attribute_map = {}
42
+ self._upsert_to_server_int64_attribute_map = {}
43
+ self._string_attribute_map = {}
44
+ self._upsert_to_server_string_attribute_map = {}
45
+ self._double_attribute_map = {}
46
+ self._upsert_to_server_double_attribute_map = {}
47
+ self._bool_attribute_map = {}
48
+ self._upsert_to_server_bool_attribute_map = {}
49
+ self._string_array_attribute_map = {}
50
+ self._upsert_to_server_string_array_attribute_map = {}
51
+
52
+ if search_attribute_map is not None:
53
+ for attribute in search_attribute_map:
54
+ key = unset_to_none(attribute.key)
55
+ if key is None:
56
+ raise ValueError("empty search attribute value type is not allowed")
57
+ value_type = key_to_type_map[key]
58
+
59
+ if (
60
+ value_type == SearchAttributeValueType.KEYWORD
61
+ or value_type == SearchAttributeValueType.DATETIME
62
+ or value_type == SearchAttributeValueType.TEXT
63
+ ):
64
+ self._string_attribute_map[key] = unset_to_none(
65
+ attribute.string_value
66
+ )
67
+ elif value_type == SearchAttributeValueType.INT:
68
+ self._int64_attribute_map[key] = unset_to_none(
69
+ attribute.integer_value
70
+ )
71
+ elif value_type == SearchAttributeValueType.DOUBLE:
72
+ self._double_attribute_map[key] = unset_to_none(
73
+ attribute.double_value
74
+ )
75
+ elif value_type == SearchAttributeValueType.BOOL:
76
+ self._bool_attribute_map[key] = unset_to_none(attribute.bool_value)
77
+ elif value_type == SearchAttributeValueType.KEYWORD_ARRAY:
78
+ self._string_array_attribute_map[key] = unset_to_none(
79
+ attribute.string_array_value
80
+ )
81
+ else:
82
+ raise ValueError(
83
+ f"not supported search attribute value type, {value_type}"
84
+ )
85
+
86
+ def get_search_attribute_int64(self, key: str) -> Union[int, None]:
87
+ return self._int64_attribute_map.get(key)
88
+
89
+ def set_search_attribute_int64(self, key: str, value: Union[int, None]):
90
+ if (
91
+ key not in self._key_to_type_map
92
+ or self._key_to_type_map[key] != SearchAttributeValueType.INT
93
+ ):
94
+ raise WorkflowDefinitionError(f"key {key} is not defined as int64")
95
+ self._int64_attribute_map[key] = value
96
+ self._upsert_to_server_int64_attribute_map[key] = value
97
+
98
+ def get_search_attribute_double(self, key: str) -> Union[float, None]:
99
+ return self._double_attribute_map.get(key)
100
+
101
+ def set_search_attribute_double(self, key: str, value: Union[float, None]):
102
+ if (
103
+ key not in self._key_to_type_map
104
+ or self._key_to_type_map[key] != SearchAttributeValueType.DOUBLE
105
+ ):
106
+ raise WorkflowDefinitionError(f"key {key} is not defined as double")
107
+ self._double_attribute_map[key] = value
108
+ self._upsert_to_server_double_attribute_map[key] = value
109
+
110
+ def get_search_attribute_boolean(self, key: str) -> Union[bool, None]:
111
+ return self._bool_attribute_map.get(key)
112
+
113
+ def set_search_attribute_boolean(self, key: str, value: Union[bool, None]):
114
+ if (
115
+ key not in self._key_to_type_map
116
+ or self._key_to_type_map[key] != SearchAttributeValueType.BOOL
117
+ ):
118
+ raise WorkflowDefinitionError(f"key {key} is not defined as bool")
119
+ self._bool_attribute_map[key] = value
120
+ self._upsert_to_server_bool_attribute_map[key] = value
121
+
122
+ def get_search_attribute_keyword(self, key: str) -> Union[str, None]:
123
+ return self._string_attribute_map.get(key)
124
+
125
+ def set_search_attribute_keyword(self, key: str, value: Union[str, None]):
126
+ if (
127
+ key not in self._key_to_type_map
128
+ or self._key_to_type_map[key] != SearchAttributeValueType.KEYWORD
129
+ ):
130
+ raise WorkflowDefinitionError(f"key {key} is not defined as keyword")
131
+ self._string_attribute_map[key] = value
132
+ self._upsert_to_server_string_attribute_map[key] = value
133
+
134
+ def get_search_attribute_text(self, key: str) -> Union[str, None]:
135
+ return self._string_attribute_map.get(key)
136
+
137
+ def set_search_attribute_text(self, key: str, value: Union[str, None]):
138
+ if (
139
+ key not in self._key_to_type_map
140
+ or self._key_to_type_map[key] != SearchAttributeValueType.TEXT
141
+ ):
142
+ raise WorkflowDefinitionError(f"key {key} is not defined as text")
143
+ self._string_attribute_map[key] = value
144
+ self._upsert_to_server_string_attribute_map[key] = value
145
+
146
+ def get_search_attribute_datetime(self, key: str) -> Union[str, None]:
147
+ return self._string_attribute_map.get(key)
148
+
149
+ def set_search_attribute_datetime(self, key: str, value: Union[str, None]):
150
+ if (
151
+ key not in self._key_to_type_map
152
+ or self._key_to_type_map[key] != SearchAttributeValueType.DATETIME
153
+ ):
154
+ raise WorkflowDefinitionError(f"key {key} is not defined as datetime")
155
+ self._string_attribute_map[key] = value
156
+ self._upsert_to_server_string_attribute_map[key] = value
157
+
158
+ def get_search_attribute_keyword_array(self, key: str) -> Union[list[str], None]:
159
+ return self._string_array_attribute_map.get(key)
160
+
161
+ def set_search_attribute_keyword_array(
162
+ self, key: str, value: Union[list[str], None]
163
+ ):
164
+ if (
165
+ key not in self._key_to_type_map
166
+ or self._key_to_type_map[key] != SearchAttributeValueType.KEYWORD_ARRAY
167
+ ):
168
+ raise WorkflowDefinitionError(f"key {key} is not defined as keyword array")
169
+ self._string_array_attribute_map[key] = value
170
+ self._upsert_to_server_string_array_attribute_map[key] = value
171
+
172
+ def get_upsert_to_server_int64_attribute_map(self) -> dict[str, Union[int, None]]:
173
+ return self._upsert_to_server_int64_attribute_map
174
+
175
+ def get_upsert_to_server_string_attribute_map(self) -> dict[str, Union[str, None]]:
176
+ return self._upsert_to_server_string_attribute_map
177
+
178
+ def get_upsert_to_server_string_array_attribute_map(
179
+ self,
180
+ ) -> dict[str, Union[list[str], None]]:
181
+ return self._upsert_to_server_string_array_attribute_map
182
+
183
+ def get_upsert_to_server_bool_attribute_map(self) -> dict[str, Union[bool, None]]:
184
+ return self._upsert_to_server_bool_attribute_map
185
+
186
+ def get_upsert_to_server_double_attribute_map(
187
+ self,
188
+ ) -> dict[str, Union[float, None]]:
189
+ return self._upsert_to_server_double_attribute_map
dex/state_decision.py ADDED
@@ -0,0 +1,161 @@
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 __future__ import annotations # <-- Additional import.
16
+
17
+ import typing
18
+
19
+ from dex.dex_api.models import WorkflowConditionalClose, WorkflowConditionalCloseType
20
+ from dex.workflow_state_options import WorkflowStateOptions
21
+
22
+ if typing.TYPE_CHECKING:
23
+ from dex.registry import Registry
24
+ from dex.workflow_state import WorkflowState
25
+
26
+ from dataclasses import dataclass
27
+ from typing import Any, List, Union, Optional
28
+
29
+ from dex.dex_api.models.state_decision import StateDecision as IdlStateDecision
30
+
31
+ from dex.object_encoder import ObjectEncoder
32
+
33
+ from dex.state_movement import StateMovement, _to_idl_state_movement
34
+
35
+
36
+ @dataclass
37
+ class InternalConditionalClose:
38
+ conditional_close_type: WorkflowConditionalCloseType
39
+
40
+ channel_name: str
41
+
42
+ close_input: Any = None
43
+
44
+
45
+ @dataclass
46
+ class StateDecision:
47
+ next_states: List[StateMovement]
48
+
49
+ conditional_close: typing.Optional[InternalConditionalClose] = None
50
+
51
+ dead_end: typing.ClassVar[StateDecision]
52
+
53
+ @classmethod
54
+ def graceful_complete_workflow(cls, output: Any = None) -> StateDecision:
55
+ return StateDecision([StateMovement.graceful_complete_workflow(output)])
56
+
57
+ @classmethod
58
+ def force_complete_workflow(cls, output: Any = None) -> StateDecision:
59
+ return StateDecision([StateMovement.force_complete_workflow(output)])
60
+
61
+ @classmethod
62
+ def force_fail_workflow(cls, output: Any = None) -> StateDecision:
63
+ return StateDecision([StateMovement.force_fail_workflow(output)])
64
+
65
+ @classmethod
66
+ def single_next_state(
67
+ cls,
68
+ state: Union[str, type[WorkflowState]],
69
+ state_input: Any = None,
70
+ state_options_override: Optional[WorkflowStateOptions] = None,
71
+ wait_for_key: Optional[str] = None,
72
+ ) -> StateDecision:
73
+ return StateDecision(
74
+ [
75
+ StateMovement.create(
76
+ state, state_input, state_options_override, wait_for_key
77
+ )
78
+ ]
79
+ )
80
+
81
+ @classmethod
82
+ def multi_next_states(
83
+ cls, *next_states: Union[type[WorkflowState], StateMovement]
84
+ ) -> StateDecision:
85
+ next_list = [
86
+ n if isinstance(n, StateMovement) else StateMovement.create(n)
87
+ for n in next_states
88
+ ]
89
+ return StateDecision(next_list)
90
+
91
+ # Atomically force complete the workflow if internal channel is empty, otherwise trigger the state movements from the current thread
92
+ # This is to ensure all the messages in the channel are processed before completing the workflow, otherwise messages may be lost.
93
+ # Without this atomic API, if just checking the channel emptiness in the State WaitUntil, a workflow may receive new messages during the
94
+ # execution of state APIs.
95
+ #
96
+ # Note that it's only for internal messages published from RPCs.
97
+ # It doesn't cover the cases that internal messages are published from other State APIs.
98
+ # If you do want to use other State APIs to publish messages to the channel at the same time, you can use persistence locking to
99
+ # ensure only the State APIs are not executed in parallel.
100
+ @classmethod
101
+ def force_complete_if_internal_channel_empty_or_else(
102
+ cls,
103
+ internal_channel_name: str,
104
+ workflow_complete_output: Any = None, # if channel is empty, complete the workflow with the output
105
+ or_else_state: Union[
106
+ str, type[WorkflowState]
107
+ ] = "", # required not empty -- if channel is NOT empty, go to this state with the state input
108
+ state_input: Any = None,
109
+ ) -> StateDecision:
110
+ return StateDecision(
111
+ [StateMovement.create(or_else_state, state_input)],
112
+ InternalConditionalClose(
113
+ WorkflowConditionalCloseType.FORCE_COMPLETE_ON_INTERNAL_CHANNEL_EMPTY,
114
+ internal_channel_name,
115
+ workflow_complete_output,
116
+ ),
117
+ )
118
+
119
+ # Atomically force complete the workflow if signal channel is empty, otherwise trigger the state movements from the current thread
120
+ # This is to ensure all the messages in the channel are processed before completing the workflow, otherwise messages may be lost.
121
+ # Without this atomic API, if just checking the channel emptiness in the State WaitUntil, a workflow may receive new messages during the
122
+ # execution of state APIs.
123
+ @classmethod
124
+ def force_complete_if_signal_channel_empty_or_else(
125
+ cls,
126
+ signal_channel_name: str,
127
+ workflow_complete_output: Any = None, # if channel is empty, complete the workflow with the output
128
+ or_else_state: Union[
129
+ str, type[WorkflowState]
130
+ ] = "", # required not empty-- if channel is NOT empty, go to this state with the state input
131
+ state_input: Any = None,
132
+ ) -> StateDecision:
133
+ return StateDecision(
134
+ [StateMovement.create(or_else_state, state_input)],
135
+ InternalConditionalClose(
136
+ WorkflowConditionalCloseType.FORCE_COMPLETE_ON_SIGNAL_CHANNEL_EMPTY,
137
+ signal_channel_name,
138
+ workflow_complete_output,
139
+ ),
140
+ )
141
+
142
+
143
+ StateDecision.dead_end = StateDecision([StateMovement.dead_end])
144
+
145
+
146
+ def _to_idl_state_decision(
147
+ decision: StateDecision, wf_type: str, registry: Registry, encoder: ObjectEncoder
148
+ ) -> IdlStateDecision:
149
+ idl_decision = IdlStateDecision()
150
+ if len(decision.next_states) > 0:
151
+ idl_decision.next_states = [
152
+ _to_idl_state_movement(movement, wf_type, registry, encoder)
153
+ for movement in decision.next_states
154
+ ]
155
+ if decision.conditional_close is not None:
156
+ idl_decision.conditional_close = WorkflowConditionalClose(
157
+ conditional_close_type=decision.conditional_close.conditional_close_type,
158
+ channel_name=decision.conditional_close.channel_name,
159
+ close_input=encoder.encode(decision.conditional_close.close_input),
160
+ )
161
+ return idl_decision
@@ -0,0 +1,71 @@
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 typing import Any, List, Tuple, Union
16
+
17
+ from dex.errors import WorkflowDefinitionError
18
+ from dex.dex_api.models import EncodedObject, KeyValue
19
+ from dex.dex_api.types import Unset
20
+ from dex.object_encoder import ObjectEncoder
21
+
22
+
23
+ class StateExecutionLocals:
24
+ _record_events: dict[str, Union[EncodedObject, Unset]]
25
+ _attribute_name_to_encoded_object_map: dict[str, Union[EncodedObject, Unset]]
26
+ _upsert_attributes_to_return_to_server: dict[str, Union[EncodedObject, Unset]]
27
+ _object_encoder: ObjectEncoder
28
+
29
+ def __init__(
30
+ self,
31
+ attribute_name_to_encoded_object_map: dict[str, Union[EncodedObject, Unset]],
32
+ object_encoder: ObjectEncoder,
33
+ ):
34
+ self._object_encoder = object_encoder
35
+ self._attribute_name_to_encoded_object_map = (
36
+ attribute_name_to_encoded_object_map
37
+ )
38
+ self._upsert_attributes_to_return_to_server = {}
39
+ self._record_events = {}
40
+
41
+ def set_state_execution_local(self, key: str, value: Any):
42
+ encoded_data = self._object_encoder.encode(value)
43
+ self._attribute_name_to_encoded_object_map[key] = encoded_data
44
+ self._upsert_attributes_to_return_to_server[key] = encoded_data
45
+
46
+ def get_state_execution_local(self, key: str) -> Any:
47
+ encoded_object = self._attribute_name_to_encoded_object_map.get(key)
48
+ if encoded_object is None:
49
+ return None
50
+ return self._object_encoder.decode(encoded_object)
51
+
52
+ def record_event(self, key: str, *event_data: Tuple[Any, ...]):
53
+ if key in self._record_events:
54
+ raise WorkflowDefinitionError("Cannot record the same event more than once")
55
+
56
+ if event_data is not None and len(event_data) == 1:
57
+ self._record_events[key] = self._object_encoder.encode(event_data[0])
58
+
59
+ self._record_events[key] = self._object_encoder.encode(event_data)
60
+
61
+ def get_upsert_state_execution_local_attributes(self) -> List[KeyValue]:
62
+ return [
63
+ KeyValue(item_key, item_value)
64
+ for item_key, item_value in self._upsert_attributes_to_return_to_server.items()
65
+ ]
66
+
67
+ def get_record_events(self) -> List[KeyValue]:
68
+ return [
69
+ KeyValue(item_key, item_value)
70
+ for item_key, item_value in self._record_events.items()
71
+ ]
dex/state_movement.py ADDED
@@ -0,0 +1,123 @@
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 __future__ import annotations
16
+
17
+ import typing
18
+ from typing import Union, Optional
19
+
20
+ from dex.errors import WorkflowDefinitionError
21
+
22
+ if typing.TYPE_CHECKING:
23
+ from dex.workflow_state import (
24
+ WorkflowState,
25
+ )
26
+ from dex.registry import Registry
27
+
28
+ from dataclasses import dataclass
29
+ from typing import Any
30
+
31
+
32
+ from dex.dex_api.models.state_movement import StateMovement as IdlStateMovement
33
+
34
+ from dex.object_encoder import ObjectEncoder
35
+
36
+ from dex.workflow_state_options import _to_idl_state_options, WorkflowStateOptions
37
+
38
+ reserved_state_id_prefix = "_SYS_"
39
+
40
+ graceful_completing_sys_state_id = (
41
+ reserved_state_id_prefix + "GRACEFUL_COMPLETING_WORKFLOW"
42
+ )
43
+ force_completing_sys_state_id = reserved_state_id_prefix + "FORCE_COMPLETING_WORKFLOW"
44
+ force_failing_sys_state_id = reserved_state_id_prefix + "FORCE_FAILING_WORKFLOW"
45
+ dead_end_sys_state_id = reserved_state_id_prefix + "DEAD_END"
46
+
47
+
48
+ @dataclass
49
+ class StateMovement:
50
+ state_id: str
51
+ state_input: Any = None
52
+ state_options_override: Optional[WorkflowStateOptions] = None
53
+ wait_for_key: Optional[str] = None
54
+
55
+ dead_end: typing.ClassVar[StateMovement]
56
+
57
+ @classmethod
58
+ def graceful_complete_workflow(cls, output: Any = None) -> StateMovement:
59
+ return StateMovement(graceful_completing_sys_state_id, output)
60
+
61
+ @classmethod
62
+ def force_complete_workflow(cls, output: Any = None) -> StateMovement:
63
+ return StateMovement(force_completing_sys_state_id, output)
64
+
65
+ @classmethod
66
+ def force_fail_workflow(cls, output: Any = None) -> StateMovement:
67
+ return StateMovement(force_failing_sys_state_id, output)
68
+
69
+ @classmethod
70
+ def create(
71
+ cls,
72
+ state: Union[str, type[WorkflowState]],
73
+ state_input: Any = None,
74
+ state_options_override: Optional[WorkflowStateOptions] = None,
75
+ wait_for_key: Optional[str] = None,
76
+ ) -> StateMovement:
77
+ if isinstance(state, str):
78
+ state_id = state
79
+ else:
80
+ from dex.workflow_state import (
81
+ get_state_id_by_class,
82
+ )
83
+
84
+ state_id = get_state_id_by_class(state)
85
+ if state_id.startswith(reserved_state_id_prefix):
86
+ raise WorkflowDefinitionError("cannot use reserved stateId")
87
+ return StateMovement(
88
+ state_id, state_input, state_options_override, wait_for_key
89
+ )
90
+
91
+
92
+ StateMovement.dead_end = StateMovement(dead_end_sys_state_id)
93
+
94
+
95
+ def _to_idl_state_movement(
96
+ movement: StateMovement, wf_type: str, registry: Registry, encoder: ObjectEncoder
97
+ ) -> IdlStateMovement:
98
+ idl_movement = IdlStateMovement(
99
+ state_id=movement.state_id, state_input=encoder.encode(movement.state_input)
100
+ )
101
+ if not movement.state_id.startswith(reserved_state_id_prefix):
102
+ state = registry.get_workflow_state_with_check(wf_type, movement.state_id)
103
+
104
+ from dex.workflow_state import (
105
+ should_skip_wait_until,
106
+ )
107
+
108
+ if movement.state_options_override is not None:
109
+ options = movement.state_options_override
110
+ else:
111
+ options = state.get_state_options()
112
+
113
+ idl_state_options = _to_idl_state_options(
114
+ should_skip_wait_until(state),
115
+ options,
116
+ registry.get_state_store(wf_type),
117
+ )
118
+
119
+ idl_movement.state_options = idl_state_options
120
+
121
+ if movement.wait_for_key is not None:
122
+ idl_movement.wait_for_key = movement.wait_for_key
123
+ return idl_movement
dex/state_schema.py ADDED
@@ -0,0 +1,54 @@
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 List
17
+
18
+ from dex.workflow_state import WorkflowState
19
+
20
+
21
+ @dataclass
22
+ class StateDef:
23
+ state: WorkflowState
24
+ can_start_workflow: bool
25
+
26
+ @classmethod
27
+ def starting_state(cls, state: WorkflowState):
28
+ return StateDef(state, True)
29
+
30
+ @classmethod
31
+ def non_starting_state(cls, state: WorkflowState):
32
+ return StateDef(state, False)
33
+
34
+
35
+ @dataclass
36
+ class StateSchema:
37
+ states: List[StateDef] = field(default_factory=list)
38
+
39
+ # TODO: it's super weird that we can't use type hint here " ->StateSchema" for return
40
+ # But the pattern works for state_movement.py
41
+ @classmethod
42
+ def with_starting_state(
43
+ cls, starting_state: WorkflowState, *non_starting_states: WorkflowState
44
+ ):
45
+ return StateSchema(
46
+ [StateDef.starting_state(starting_state)]
47
+ + [StateDef.non_starting_state(s) for s in non_starting_states]
48
+ )
49
+
50
+ @classmethod
51
+ def no_starting_state(cls, *non_starting_states: WorkflowState):
52
+ return StateSchema(
53
+ [StateDef.non_starting_state(s) for s in non_starting_states]
54
+ )
@@ -0,0 +1,23 @@
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.dex_api.models import WorkflowStopType
18
+
19
+
20
+ @dataclass
21
+ class StopWorkflowOptions:
22
+ workflow_stop_type: WorkflowStopType
23
+ reason: str