dex-python-sdk 0.0.2__cp311-abi3-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. dex/__init__.py +146 -0
  2. dex/_grpc_errors.py +73 -0
  3. dex/_invocation_context.py +269 -0
  4. dex/_native.pyd +0 -0
  5. dex/_native.pyi +20 -0
  6. dex/_utils.py +18 -0
  7. dex/_value_hydrator.py +223 -0
  8. dex/_value_mapper.py +174 -0
  9. dex/_worker_dispatcher.py +451 -0
  10. dex/_worker_service.py +60 -0
  11. dex/attribute.py +87 -0
  12. dex/blob_cache.py +73 -0
  13. dex/channel.py +165 -0
  14. dex/client.py +715 -0
  15. dex/client_options.py +19 -0
  16. dex/codec.py +286 -0
  17. dex/command_request.py +120 -0
  18. dex/command_results.py +107 -0
  19. dex/communication.py +136 -0
  20. dex/communication_schema.py +54 -0
  21. dex/condition.py +74 -0
  22. dex/context.py +86 -0
  23. dex/data_attributes.py +70 -0
  24. dex/dexpb/__init__.py +1 -0
  25. dex/dexpb/dex_pb2.py +381 -0
  26. dex/dexpb/dex_pb2.pyi +1734 -0
  27. dex/dexpb/dex_pb2_grpc.py +1298 -0
  28. dex/errors.py +109 -0
  29. dex/flow.py +456 -0
  30. dex/flow_config.py +29 -0
  31. dex/flow_info.py +51 -0
  32. dex/flow_options.py +122 -0
  33. dex/object_encoder.py +799 -0
  34. dex/persistence.py +89 -0
  35. dex/persistence_options.py +12 -0
  36. dex/persistence_schema.py +51 -0
  37. dex/py.typed +1 -0
  38. dex/registry.py +204 -0
  39. dex/reset_workflow_type_and_options.py +67 -0
  40. dex/rpc.py +93 -0
  41. dex/runtime_errors.py +81 -0
  42. dex/search_attributes.py +184 -0
  43. dex/state_decision.py +153 -0
  44. dex/state_execution_locals.py +66 -0
  45. dex/state_movement.py +115 -0
  46. dex/state_schema.py +48 -0
  47. dex/step.py +194 -0
  48. dex/step_execution.py +42 -0
  49. dex/stop_workflow_options.py +18 -0
  50. dex/tests/__init__.py +80 -0
  51. dex/tests/dex-service-env/.env +7 -0
  52. dex/tests/dex-service-env/docker-compose-init.sh +44 -0
  53. dex/tests/dex-service-env/docker-compose.yml +97 -0
  54. dex/tests/dex-service-env/dynamicconfig/README.md +39 -0
  55. dex/tests/dex-service-env/dynamicconfig/development-sql.yaml +9 -0
  56. dex/tests/dex-service-env/dynamicconfig/docker.yaml +2 -0
  57. dex/tests/test_abnormal_exit_workflow.py +43 -0
  58. dex/tests/test_basic_workflow.py +70 -0
  59. dex/tests/test_conditional_complete.py +50 -0
  60. dex/tests/test_describe_workflow.py +40 -0
  61. dex/tests/test_empty_data_decodes_properly.py +74 -0
  62. dex/tests/test_internal_channel.py +28 -0
  63. dex/tests/test_internal_channel_with_no_prefix_channel.py +41 -0
  64. dex/tests/test_persistence_data_attributes.py +62 -0
  65. dex/tests/test_persistence_search_attributes.py +127 -0
  66. dex/tests/test_persistence_state_execution_locals.py +38 -0
  67. dex/tests/test_rpc.py +64 -0
  68. dex/tests/test_rpc_with_memo.py +195 -0
  69. dex/tests/test_rpc_with_memo_duplicate_java_tests.py +117 -0
  70. dex/tests/test_signal.py +51 -0
  71. dex/tests/test_skip_wait_until.py +76 -0
  72. dex/tests/test_state_failure_recovery.py +28 -0
  73. dex/tests/test_timer.py +35 -0
  74. dex/tests/test_wait_for_state_execution_completion.py +53 -0
  75. dex/tests/test_workflow_errors.py +87 -0
  76. dex/tests/test_workflow_state_options.py +118 -0
  77. dex/tests/test_workflow_state_options_override.py +44 -0
  78. dex/tests/worker_server.py +64 -0
  79. dex/tests/workflows/abnormal_exit_workflow.py +42 -0
  80. dex/tests/workflows/basic_workflow.py +62 -0
  81. dex/tests/workflows/conditional_complete_workflow.py +95 -0
  82. dex/tests/workflows/describe_workflow.py +46 -0
  83. dex/tests/workflows/empty_data_workflow.py +45 -0
  84. dex/tests/workflows/internal_channel_workflow.py +129 -0
  85. dex/tests/workflows/internal_channel_workflow_with_no_prefix_channel.py +100 -0
  86. dex/tests/workflows/java_duplicate_rpc_memo_workflow.py +276 -0
  87. dex/tests/workflows/persistence_data_attributes_workflow.py +98 -0
  88. dex/tests/workflows/persistence_search_attributes_workflow.py +159 -0
  89. dex/tests/workflows/persistence_state_execution_local_workflow.py +63 -0
  90. dex/tests/workflows/recovery_workflow.py +82 -0
  91. dex/tests/workflows/rpc_memo_workflow.py +231 -0
  92. dex/tests/workflows/rpc_workflow.py +117 -0
  93. dex/tests/workflows/state_options_override_workflow.py +93 -0
  94. dex/tests/workflows/state_options_workflow.py +84 -0
  95. dex/tests/workflows/timer_workflow.py +46 -0
  96. dex/tests/workflows/wait_for_state_with_state_execution_id_workflow.py +70 -0
  97. dex/tests/workflows/wait_for_state_with_wait_for_key_workflow.py +71 -0
  98. dex/tests/workflows/wait_internal_channel_workflow.py +47 -0
  99. dex/tests/workflows/wait_signal_workflow.py +147 -0
  100. dex/timer.py +21 -0
  101. dex/type_store.py +99 -0
  102. dex/unregistered_client.py +585 -0
  103. dex/utils/__init__.py +3 -0
  104. dex/utils/dex_typing.py +25 -0
  105. dex/utils/persistence_utils.py +32 -0
  106. dex/wait.py +49 -0
  107. dex/worker.py +121 -0
  108. dex/worker_options.py +22 -0
  109. dex/worker_service.py +432 -0
  110. dex/workflow.py +79 -0
  111. dex/workflow_context.py +44 -0
  112. dex/workflow_info.py +16 -0
  113. dex/workflow_options.py +74 -0
  114. dex/workflow_state.py +123 -0
  115. dex/workflow_state_options.py +154 -0
  116. dex_python_sdk-0.0.2.dist-info/METADATA +202 -0
  117. dex_python_sdk-0.0.2.dist-info/RECORD +121 -0
  118. dex_python_sdk-0.0.2.dist-info/WHEEL +4 -0
  119. dex_python_sdk-0.0.2.dist-info/licenses/LEGACY_NOTICES.md +61 -0
  120. dex_python_sdk-0.0.2.dist-info/licenses/LICENSE +192 -0
  121. dex_python_sdk-0.0.2.dist-info/sboms/dex-blob-cache-python.cyclonedx.json +2406 -0
@@ -0,0 +1,184 @@
1
+ # Legacy Materials in this file remain under their original licenses.
2
+ # See LEGACY_NOTICES.md.
3
+
4
+ # Modifications Copyright (c) 2026 Super Durable, Inc.
5
+ #
6
+ # Modifications after the Legacy Cutoff are licensed under the
7
+ # Super Durable Source License 1.0.
8
+ # Legacy Materials remain under their original licenses.
9
+ # See LICENSE and LEGACY_NOTICES.md.
10
+
11
+ from typing import Union
12
+
13
+ from dex.errors import WorkflowDefinitionError
14
+ from dex.dex_api.models import SearchAttribute, SearchAttributeValueType
15
+ from dex.utils.dex_typing import unset_to_none
16
+
17
+ class SearchAttributes:
18
+ _key_to_type_map: dict[str, SearchAttributeValueType]
19
+ _int64_attribute_map: dict[str, Union[int, None]]
20
+ _upsert_to_server_int64_attribute_map: dict[str, Union[int, None]]
21
+ _string_attribute_map: dict[str, Union[str, None]]
22
+ _upsert_to_server_string_attribute_map: dict[str, Union[str, None]]
23
+ _double_attribute_map: dict[str, Union[float, None]]
24
+ _upsert_to_server_double_attribute_map: dict[str, Union[float, None]]
25
+ _bool_attribute_map: dict[str, Union[bool, None]]
26
+ _upsert_to_server_bool_attribute_map: dict[str, Union[bool, None]]
27
+ _string_array_attribute_map: dict[str, Union[list[str], None]]
28
+ _upsert_to_server_string_array_attribute_map: dict[str, Union[list[str], None]]
29
+
30
+ def __init__(
31
+ self,
32
+ key_to_type_map: dict[str, SearchAttributeValueType],
33
+ search_attribute_map: Union[list[SearchAttribute], None],
34
+ ):
35
+ self._key_to_type_map = key_to_type_map
36
+ self._int64_attribute_map = {}
37
+ self._upsert_to_server_int64_attribute_map = {}
38
+ self._string_attribute_map = {}
39
+ self._upsert_to_server_string_attribute_map = {}
40
+ self._double_attribute_map = {}
41
+ self._upsert_to_server_double_attribute_map = {}
42
+ self._bool_attribute_map = {}
43
+ self._upsert_to_server_bool_attribute_map = {}
44
+ self._string_array_attribute_map = {}
45
+ self._upsert_to_server_string_array_attribute_map = {}
46
+
47
+ if search_attribute_map is not None:
48
+ for attribute in search_attribute_map:
49
+ key = unset_to_none(attribute.key)
50
+ if key is None:
51
+ raise ValueError("empty search attribute value type is not allowed")
52
+ value_type = key_to_type_map[key]
53
+
54
+ if (
55
+ value_type == SearchAttributeValueType.KEYWORD
56
+ or value_type == SearchAttributeValueType.DATETIME
57
+ or value_type == SearchAttributeValueType.TEXT
58
+ ):
59
+ self._string_attribute_map[key] = unset_to_none(
60
+ attribute.string_value
61
+ )
62
+ elif value_type == SearchAttributeValueType.INT:
63
+ self._int64_attribute_map[key] = unset_to_none(
64
+ attribute.integer_value
65
+ )
66
+ elif value_type == SearchAttributeValueType.DOUBLE:
67
+ self._double_attribute_map[key] = unset_to_none(
68
+ attribute.double_value
69
+ )
70
+ elif value_type == SearchAttributeValueType.BOOL:
71
+ self._bool_attribute_map[key] = unset_to_none(attribute.bool_value)
72
+ elif value_type == SearchAttributeValueType.KEYWORD_ARRAY:
73
+ self._string_array_attribute_map[key] = unset_to_none(
74
+ attribute.string_array_value
75
+ )
76
+ else:
77
+ raise ValueError(
78
+ f"not supported search attribute value type, {value_type}"
79
+ )
80
+
81
+ def get_search_attribute_int64(self, key: str) -> Union[int, None]:
82
+ return self._int64_attribute_map.get(key)
83
+
84
+ def set_search_attribute_int64(self, key: str, value: Union[int, None]):
85
+ if (
86
+ key not in self._key_to_type_map
87
+ or self._key_to_type_map[key] != SearchAttributeValueType.INT
88
+ ):
89
+ raise WorkflowDefinitionError(f"key {key} is not defined as int64")
90
+ self._int64_attribute_map[key] = value
91
+ self._upsert_to_server_int64_attribute_map[key] = value
92
+
93
+ def get_search_attribute_double(self, key: str) -> Union[float, None]:
94
+ return self._double_attribute_map.get(key)
95
+
96
+ def set_search_attribute_double(self, key: str, value: Union[float, None]):
97
+ if (
98
+ key not in self._key_to_type_map
99
+ or self._key_to_type_map[key] != SearchAttributeValueType.DOUBLE
100
+ ):
101
+ raise WorkflowDefinitionError(f"key {key} is not defined as double")
102
+ self._double_attribute_map[key] = value
103
+ self._upsert_to_server_double_attribute_map[key] = value
104
+
105
+ def get_search_attribute_boolean(self, key: str) -> Union[bool, None]:
106
+ return self._bool_attribute_map.get(key)
107
+
108
+ def set_search_attribute_boolean(self, key: str, value: Union[bool, None]):
109
+ if (
110
+ key not in self._key_to_type_map
111
+ or self._key_to_type_map[key] != SearchAttributeValueType.BOOL
112
+ ):
113
+ raise WorkflowDefinitionError(f"key {key} is not defined as bool")
114
+ self._bool_attribute_map[key] = value
115
+ self._upsert_to_server_bool_attribute_map[key] = value
116
+
117
+ def get_search_attribute_keyword(self, key: str) -> Union[str, None]:
118
+ return self._string_attribute_map.get(key)
119
+
120
+ def set_search_attribute_keyword(self, key: str, value: Union[str, None]):
121
+ if (
122
+ key not in self._key_to_type_map
123
+ or self._key_to_type_map[key] != SearchAttributeValueType.KEYWORD
124
+ ):
125
+ raise WorkflowDefinitionError(f"key {key} is not defined as keyword")
126
+ self._string_attribute_map[key] = value
127
+ self._upsert_to_server_string_attribute_map[key] = value
128
+
129
+ def get_search_attribute_text(self, key: str) -> Union[str, None]:
130
+ return self._string_attribute_map.get(key)
131
+
132
+ def set_search_attribute_text(self, key: str, value: Union[str, None]):
133
+ if (
134
+ key not in self._key_to_type_map
135
+ or self._key_to_type_map[key] != SearchAttributeValueType.TEXT
136
+ ):
137
+ raise WorkflowDefinitionError(f"key {key} is not defined as text")
138
+ self._string_attribute_map[key] = value
139
+ self._upsert_to_server_string_attribute_map[key] = value
140
+
141
+ def get_search_attribute_datetime(self, key: str) -> Union[str, None]:
142
+ return self._string_attribute_map.get(key)
143
+
144
+ def set_search_attribute_datetime(self, key: str, value: Union[str, None]):
145
+ if (
146
+ key not in self._key_to_type_map
147
+ or self._key_to_type_map[key] != SearchAttributeValueType.DATETIME
148
+ ):
149
+ raise WorkflowDefinitionError(f"key {key} is not defined as datetime")
150
+ self._string_attribute_map[key] = value
151
+ self._upsert_to_server_string_attribute_map[key] = value
152
+
153
+ def get_search_attribute_keyword_array(self, key: str) -> Union[list[str], None]:
154
+ return self._string_array_attribute_map.get(key)
155
+
156
+ def set_search_attribute_keyword_array(
157
+ self, key: str, value: Union[list[str], None]
158
+ ):
159
+ if (
160
+ key not in self._key_to_type_map
161
+ or self._key_to_type_map[key] != SearchAttributeValueType.KEYWORD_ARRAY
162
+ ):
163
+ raise WorkflowDefinitionError(f"key {key} is not defined as keyword array")
164
+ self._string_array_attribute_map[key] = value
165
+ self._upsert_to_server_string_array_attribute_map[key] = value
166
+
167
+ def get_upsert_to_server_int64_attribute_map(self) -> dict[str, Union[int, None]]:
168
+ return self._upsert_to_server_int64_attribute_map
169
+
170
+ def get_upsert_to_server_string_attribute_map(self) -> dict[str, Union[str, None]]:
171
+ return self._upsert_to_server_string_attribute_map
172
+
173
+ def get_upsert_to_server_string_array_attribute_map(
174
+ self,
175
+ ) -> dict[str, Union[list[str], None]]:
176
+ return self._upsert_to_server_string_array_attribute_map
177
+
178
+ def get_upsert_to_server_bool_attribute_map(self) -> dict[str, Union[bool, None]]:
179
+ return self._upsert_to_server_bool_attribute_map
180
+
181
+ def get_upsert_to_server_double_attribute_map(
182
+ self,
183
+ ) -> dict[str, Union[float, None]]:
184
+ return self._upsert_to_server_double_attribute_map
dex/state_decision.py ADDED
@@ -0,0 +1,153 @@
1
+ # Legacy Materials in this file remain under their original licenses.
2
+ # See LEGACY_NOTICES.md.
3
+
4
+ # Modifications Copyright (c) 2026 Super Durable, Inc.
5
+ #
6
+ # Modifications after the Legacy Cutoff are licensed under the
7
+ # Super Durable Source License 1.0.
8
+ # Legacy Materials remain under their original licenses.
9
+ # See LICENSE and LEGACY_NOTICES.md.
10
+
11
+ from __future__ import annotations # <-- Additional import.
12
+
13
+ import typing
14
+
15
+ from dex.dex_api.models import WorkflowConditionalClose, WorkflowConditionalCloseType
16
+ from dex.workflow_state_options import WorkflowStateOptions
17
+
18
+ if typing.TYPE_CHECKING:
19
+ from dex.registry import Registry
20
+ from dex.workflow_state import WorkflowState
21
+
22
+ from dataclasses import dataclass
23
+ from typing import Any, List, Union, Optional
24
+
25
+ from dex.dex_api.models.state_decision import StateDecision as IdlStateDecision
26
+
27
+ from dex.object_encoder import ObjectEncoder
28
+
29
+ from dex.state_movement import StateMovement, _to_idl_state_movement
30
+
31
+ @dataclass
32
+ class InternalConditionalClose:
33
+ conditional_close_type: WorkflowConditionalCloseType
34
+
35
+ channel_name: str
36
+
37
+ close_input: Any = None
38
+
39
+ @dataclass
40
+ class StateDecision:
41
+ next_states: List[StateMovement]
42
+
43
+ conditional_close: typing.Optional[InternalConditionalClose] = None
44
+
45
+ dead_end: typing.ClassVar[StateDecision]
46
+
47
+ @classmethod
48
+ def graceful_complete_workflow(cls, output: Any = None) -> StateDecision:
49
+ return StateDecision([StateMovement.graceful_complete_workflow(output)])
50
+
51
+ @classmethod
52
+ def force_complete_workflow(cls, output: Any = None) -> StateDecision:
53
+ return StateDecision([StateMovement.force_complete_workflow(output)])
54
+
55
+ @classmethod
56
+ def force_fail_workflow(cls, output: Any = None) -> StateDecision:
57
+ return StateDecision([StateMovement.force_fail_workflow(output)])
58
+
59
+ @classmethod
60
+ def single_next_state(
61
+ cls,
62
+ state: Union[str, type[WorkflowState]],
63
+ state_input: Any = None,
64
+ state_options_override: Optional[WorkflowStateOptions] = None,
65
+ wait_for_key: Optional[str] = None,
66
+ ) -> StateDecision:
67
+ return StateDecision(
68
+ [
69
+ StateMovement.create(
70
+ state, state_input, state_options_override, wait_for_key
71
+ )
72
+ ]
73
+ )
74
+
75
+ @classmethod
76
+ def multi_next_states(
77
+ cls, *next_states: Union[type[WorkflowState], StateMovement]
78
+ ) -> StateDecision:
79
+ next_list = [
80
+ n if isinstance(n, StateMovement) else StateMovement.create(n)
81
+ for n in next_states
82
+ ]
83
+ return StateDecision(next_list)
84
+
85
+ # Atomically force complete the workflow if internal channel is empty, otherwise trigger the state movements from the current thread
86
+ # This is to ensure all the messages in the channel are processed before completing the workflow, otherwise messages may be lost.
87
+ # Without this atomic API, if just checking the channel emptiness in the State WaitUntil, a workflow may receive new messages during the
88
+ # execution of state APIs.
89
+ #
90
+ # Note that it's only for internal messages published from RPCs.
91
+ # It doesn't cover the cases that internal messages are published from other State APIs.
92
+ # 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
93
+ # ensure only the State APIs are not executed in parallel.
94
+ @classmethod
95
+ def force_complete_if_internal_channel_empty_or_else(
96
+ cls,
97
+ internal_channel_name: str,
98
+ workflow_complete_output: Any = None, # if channel is empty, complete the workflow with the output
99
+ or_else_state: Union[
100
+ str, type[WorkflowState]
101
+ ] = "", # required not empty -- if channel is NOT empty, go to this state with the state input
102
+ state_input: Any = None,
103
+ ) -> StateDecision:
104
+ return StateDecision(
105
+ [StateMovement.create(or_else_state, state_input)],
106
+ InternalConditionalClose(
107
+ WorkflowConditionalCloseType.FORCE_COMPLETE_ON_INTERNAL_CHANNEL_EMPTY,
108
+ internal_channel_name,
109
+ workflow_complete_output,
110
+ ),
111
+ )
112
+
113
+ # Atomically force complete the workflow if signal channel is empty, otherwise trigger the state movements from the current thread
114
+ # This is to ensure all the messages in the channel are processed before completing the workflow, otherwise messages may be lost.
115
+ # Without this atomic API, if just checking the channel emptiness in the State WaitUntil, a workflow may receive new messages during the
116
+ # execution of state APIs.
117
+ @classmethod
118
+ def force_complete_if_signal_channel_empty_or_else(
119
+ cls,
120
+ signal_channel_name: str,
121
+ workflow_complete_output: Any = None, # if channel is empty, complete the workflow with the output
122
+ or_else_state: Union[
123
+ str, type[WorkflowState]
124
+ ] = "", # required not empty-- if channel is NOT empty, go to this state with the state input
125
+ state_input: Any = None,
126
+ ) -> StateDecision:
127
+ return StateDecision(
128
+ [StateMovement.create(or_else_state, state_input)],
129
+ InternalConditionalClose(
130
+ WorkflowConditionalCloseType.FORCE_COMPLETE_ON_SIGNAL_CHANNEL_EMPTY,
131
+ signal_channel_name,
132
+ workflow_complete_output,
133
+ ),
134
+ )
135
+
136
+ StateDecision.dead_end = StateDecision([StateMovement.dead_end])
137
+
138
+ def _to_idl_state_decision(
139
+ decision: StateDecision, wf_type: str, registry: Registry, encoder: ObjectEncoder
140
+ ) -> IdlStateDecision:
141
+ idl_decision = IdlStateDecision()
142
+ if len(decision.next_states) > 0:
143
+ idl_decision.next_states = [
144
+ _to_idl_state_movement(movement, wf_type, registry, encoder)
145
+ for movement in decision.next_states
146
+ ]
147
+ if decision.conditional_close is not None:
148
+ idl_decision.conditional_close = WorkflowConditionalClose(
149
+ conditional_close_type=decision.conditional_close.conditional_close_type,
150
+ channel_name=decision.conditional_close.channel_name,
151
+ close_input=encoder.encode(decision.conditional_close.close_input),
152
+ )
153
+ return idl_decision
@@ -0,0 +1,66 @@
1
+ # Legacy Materials in this file remain under their original licenses.
2
+ # See LEGACY_NOTICES.md.
3
+
4
+ # Modifications Copyright (c) 2026 Super Durable, Inc.
5
+ #
6
+ # Modifications after the Legacy Cutoff are licensed under the
7
+ # Super Durable Source License 1.0.
8
+ # Legacy Materials remain under their original licenses.
9
+ # See LICENSE and LEGACY_NOTICES.md.
10
+
11
+ from typing import Any, List, Tuple, Union
12
+
13
+ from dex.errors import WorkflowDefinitionError
14
+ from dex.dex_api.models import EncodedObject, KeyValue
15
+ from dex.dex_api.types import Unset
16
+ from dex.object_encoder import ObjectEncoder
17
+
18
+ class StateExecutionLocals:
19
+ _record_events: dict[str, Union[EncodedObject, Unset]]
20
+ _attribute_name_to_encoded_object_map: dict[str, Union[EncodedObject, Unset]]
21
+ _upsert_attributes_to_return_to_server: dict[str, Union[EncodedObject, Unset]]
22
+ _object_encoder: ObjectEncoder
23
+
24
+ def __init__(
25
+ self,
26
+ attribute_name_to_encoded_object_map: dict[str, Union[EncodedObject, Unset]],
27
+ object_encoder: ObjectEncoder,
28
+ ):
29
+ self._object_encoder = object_encoder
30
+ self._attribute_name_to_encoded_object_map = (
31
+ attribute_name_to_encoded_object_map
32
+ )
33
+ self._upsert_attributes_to_return_to_server = {}
34
+ self._record_events = {}
35
+
36
+ def set_state_execution_local(self, key: str, value: Any):
37
+ encoded_data = self._object_encoder.encode(value)
38
+ self._attribute_name_to_encoded_object_map[key] = encoded_data
39
+ self._upsert_attributes_to_return_to_server[key] = encoded_data
40
+
41
+ def get_state_execution_local(self, key: str) -> Any:
42
+ encoded_object = self._attribute_name_to_encoded_object_map.get(key)
43
+ if encoded_object is None:
44
+ return None
45
+ return self._object_encoder.decode(encoded_object)
46
+
47
+ def record_event(self, key: str, *event_data: Tuple[Any, ...]):
48
+ if key in self._record_events:
49
+ raise WorkflowDefinitionError("Cannot record the same event more than once")
50
+
51
+ if event_data is not None and len(event_data) == 1:
52
+ self._record_events[key] = self._object_encoder.encode(event_data[0])
53
+
54
+ self._record_events[key] = self._object_encoder.encode(event_data)
55
+
56
+ def get_upsert_state_execution_local_attributes(self) -> List[KeyValue]:
57
+ return [
58
+ KeyValue(item_key, item_value)
59
+ for item_key, item_value in self._upsert_attributes_to_return_to_server.items()
60
+ ]
61
+
62
+ def get_record_events(self) -> List[KeyValue]:
63
+ return [
64
+ KeyValue(item_key, item_value)
65
+ for item_key, item_value in self._record_events.items()
66
+ ]
dex/state_movement.py ADDED
@@ -0,0 +1,115 @@
1
+ # Legacy Materials in this file remain under their original licenses.
2
+ # See LEGACY_NOTICES.md.
3
+
4
+ # Modifications Copyright (c) 2026 Super Durable, Inc.
5
+ #
6
+ # Modifications after the Legacy Cutoff are licensed under the
7
+ # Super Durable Source License 1.0.
8
+ # Legacy Materials remain under their original licenses.
9
+ # See LICENSE and LEGACY_NOTICES.md.
10
+
11
+ from __future__ import annotations
12
+
13
+ import typing
14
+ from typing import Union, Optional
15
+
16
+ from dex.errors import WorkflowDefinitionError
17
+
18
+ if typing.TYPE_CHECKING:
19
+ from dex.workflow_state import (
20
+ WorkflowState,
21
+ )
22
+ from dex.registry import Registry
23
+
24
+ from dataclasses import dataclass
25
+ from typing import Any
26
+
27
+ from dex.dex_api.models.state_movement import StateMovement as IdlStateMovement
28
+
29
+ from dex.object_encoder import ObjectEncoder
30
+
31
+ from dex.workflow_state_options import _to_idl_state_options, WorkflowStateOptions
32
+
33
+ reserved_state_id_prefix = "_SYS_"
34
+
35
+ graceful_completing_sys_state_id = (
36
+ reserved_state_id_prefix + "GRACEFUL_COMPLETING_WORKFLOW"
37
+ )
38
+ force_completing_sys_state_id = reserved_state_id_prefix + "FORCE_COMPLETING_WORKFLOW"
39
+ force_failing_sys_state_id = reserved_state_id_prefix + "FORCE_FAILING_WORKFLOW"
40
+ dead_end_sys_state_id = reserved_state_id_prefix + "DEAD_END"
41
+
42
+ @dataclass
43
+ class StateMovement:
44
+ state_id: str
45
+ state_input: Any = None
46
+ state_options_override: Optional[WorkflowStateOptions] = None
47
+ wait_for_key: Optional[str] = None
48
+
49
+ dead_end: typing.ClassVar[StateMovement]
50
+
51
+ @classmethod
52
+ def graceful_complete_workflow(cls, output: Any = None) -> StateMovement:
53
+ return StateMovement(graceful_completing_sys_state_id, output)
54
+
55
+ @classmethod
56
+ def force_complete_workflow(cls, output: Any = None) -> StateMovement:
57
+ return StateMovement(force_completing_sys_state_id, output)
58
+
59
+ @classmethod
60
+ def force_fail_workflow(cls, output: Any = None) -> StateMovement:
61
+ return StateMovement(force_failing_sys_state_id, output)
62
+
63
+ @classmethod
64
+ def create(
65
+ cls,
66
+ state: Union[str, type[WorkflowState]],
67
+ state_input: Any = None,
68
+ state_options_override: Optional[WorkflowStateOptions] = None,
69
+ wait_for_key: Optional[str] = None,
70
+ ) -> StateMovement:
71
+ if isinstance(state, str):
72
+ state_id = state
73
+ else:
74
+ from dex.workflow_state import (
75
+ get_state_id_by_class,
76
+ )
77
+
78
+ state_id = get_state_id_by_class(state)
79
+ if state_id.startswith(reserved_state_id_prefix):
80
+ raise WorkflowDefinitionError("cannot use reserved stateId")
81
+ return StateMovement(
82
+ state_id, state_input, state_options_override, wait_for_key
83
+ )
84
+
85
+ StateMovement.dead_end = StateMovement(dead_end_sys_state_id)
86
+
87
+ def _to_idl_state_movement(
88
+ movement: StateMovement, wf_type: str, registry: Registry, encoder: ObjectEncoder
89
+ ) -> IdlStateMovement:
90
+ idl_movement = IdlStateMovement(
91
+ state_id=movement.state_id, state_input=encoder.encode(movement.state_input)
92
+ )
93
+ if not movement.state_id.startswith(reserved_state_id_prefix):
94
+ state = registry.get_workflow_state_with_check(wf_type, movement.state_id)
95
+
96
+ from dex.workflow_state import (
97
+ should_skip_wait_until,
98
+ )
99
+
100
+ if movement.state_options_override is not None:
101
+ options = movement.state_options_override
102
+ else:
103
+ options = state.get_state_options()
104
+
105
+ idl_state_options = _to_idl_state_options(
106
+ should_skip_wait_until(state),
107
+ options,
108
+ registry.get_state_store(wf_type),
109
+ )
110
+
111
+ idl_movement.state_options = idl_state_options
112
+
113
+ if movement.wait_for_key is not None:
114
+ idl_movement.wait_for_key = movement.wait_for_key
115
+ return idl_movement
dex/state_schema.py ADDED
@@ -0,0 +1,48 @@
1
+ # Legacy Materials in this file remain under their original licenses.
2
+ # See LEGACY_NOTICES.md.
3
+
4
+ # Modifications Copyright (c) 2026 Super Durable, Inc.
5
+ #
6
+ # Modifications after the Legacy Cutoff are licensed under the
7
+ # Super Durable Source License 1.0.
8
+ # Legacy Materials remain under their original licenses.
9
+ # See LICENSE and LEGACY_NOTICES.md.
10
+
11
+ from dataclasses import dataclass, field
12
+ from typing import List
13
+
14
+ from dex.workflow_state import WorkflowState
15
+
16
+ @dataclass
17
+ class StateDef:
18
+ state: WorkflowState
19
+ can_start_workflow: bool
20
+
21
+ @classmethod
22
+ def starting_state(cls, state: WorkflowState):
23
+ return StateDef(state, True)
24
+
25
+ @classmethod
26
+ def non_starting_state(cls, state: WorkflowState):
27
+ return StateDef(state, False)
28
+
29
+ @dataclass
30
+ class StateSchema:
31
+ states: List[StateDef] = field(default_factory=list)
32
+
33
+ # TODO: it's super weird that we can't use type hint here " ->StateSchema" for return
34
+ # But the pattern works for state_movement.py
35
+ @classmethod
36
+ def with_starting_state(
37
+ cls, starting_state: WorkflowState, *non_starting_states: WorkflowState
38
+ ):
39
+ return StateSchema(
40
+ [StateDef.starting_state(starting_state)]
41
+ + [StateDef.non_starting_state(s) for s in non_starting_states]
42
+ )
43
+
44
+ @classmethod
45
+ def no_starting_state(cls, *non_starting_states: WorkflowState):
46
+ return StateSchema(
47
+ [StateDef.non_starting_state(s) for s in non_starting_states]
48
+ )