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
dex/persistence.py ADDED
@@ -0,0 +1,89 @@
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, Tuple, Union
12
+
13
+ from dex.data_attributes import DataAttributes
14
+ from dex.search_attributes import SearchAttributes
15
+ from dex.state_execution_locals import StateExecutionLocals
16
+
17
+ class Persistence:
18
+ _data_attributes: DataAttributes
19
+ _search_attributes: SearchAttributes
20
+ _state_execution_locals: StateExecutionLocals
21
+
22
+ def __init__(
23
+ self,
24
+ data_attributes: DataAttributes,
25
+ search_attributes: SearchAttributes,
26
+ state_execution_locals: StateExecutionLocals,
27
+ ):
28
+ self._data_attributes = data_attributes
29
+ self._search_attributes = search_attributes
30
+ self._state_execution_locals = state_execution_locals
31
+
32
+ def get_data_attribute(self, key: str) -> Any:
33
+ return self._data_attributes.get_data_attribute(key)
34
+
35
+ def set_data_attribute(self, key: str, value: Any):
36
+ self._data_attributes.set_data_attribute(key, value)
37
+
38
+ def get_search_attribute_int64(self, key: str) -> Union[None, int]:
39
+ return self._search_attributes.get_search_attribute_int64(key)
40
+
41
+ def set_search_attribute_int64(self, key: str, value: Union[None, int]):
42
+ self._search_attributes.set_search_attribute_int64(key, value)
43
+
44
+ def get_search_attribute_double(self, key: str) -> Union[None, float]:
45
+ return self._search_attributes.get_search_attribute_double(key)
46
+
47
+ def set_search_attribute_double(self, key: str, value: Union[None, float]):
48
+ self._search_attributes.set_search_attribute_double(key, value)
49
+
50
+ def get_search_attribute_boolean(self, key: str) -> Union[None, bool]:
51
+ return self._search_attributes.get_search_attribute_boolean(key)
52
+
53
+ def set_search_attribute_boolean(self, key: str, value: Union[None, bool]):
54
+ self._search_attributes.set_search_attribute_boolean(key, value)
55
+
56
+ def get_search_attribute_keyword(self, key: str) -> Union[None, str]:
57
+ return self._search_attributes.get_search_attribute_keyword(key)
58
+
59
+ def set_search_attribute_keyword(self, key: str, value: Union[None, str]):
60
+ self._search_attributes.set_search_attribute_keyword(key, value)
61
+
62
+ def get_search_attribute_text(self, key: str) -> Union[None, str]:
63
+ return self._search_attributes.get_search_attribute_text(key)
64
+
65
+ def set_search_attribute_text(self, key: str, value: Union[None, str]):
66
+ self._search_attributes.set_search_attribute_text(key, value)
67
+
68
+ def get_search_attribute_datetime(self, key: str) -> Union[None, str]:
69
+ return self._search_attributes.get_search_attribute_datetime(key)
70
+
71
+ def set_search_attribute_datetime(self, key: str, value: Union[None, str]):
72
+ self._search_attributes.set_search_attribute_datetime(key, value)
73
+
74
+ def get_search_attribute_keyword_array(self, key: str) -> Union[None, list[str]]:
75
+ return self._search_attributes.get_search_attribute_keyword_array(key)
76
+
77
+ def set_search_attribute_keyword_array(
78
+ self, key: str, value: Union[None, list[str]]
79
+ ):
80
+ self._search_attributes.set_search_attribute_keyword_array(key, value)
81
+
82
+ def get_state_execution_local(self, key: str) -> Any:
83
+ return self._state_execution_locals.get_state_execution_local(key)
84
+
85
+ def set_state_execution_local(self, key: str, value: Any):
86
+ self._state_execution_locals.set_state_execution_local(key, value)
87
+
88
+ def record_event(self, key: str, *event_data: Tuple[Any, ...]):
89
+ self._state_execution_locals.record_event(key, *event_data)
@@ -0,0 +1,12 @@
1
+ # Legacy Materials in this file remain under their original licenses.
2
+ # See LEGACY_NOTICES.md.
3
+
4
+ from dataclasses import dataclass
5
+
6
+ @dataclass
7
+ class PersistenceOptions:
8
+ enable_caching: bool
9
+
10
+ @classmethod
11
+ def get_default(cls):
12
+ return PersistenceOptions(False)
@@ -0,0 +1,51 @@
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 enum import Enum
13
+ from typing import List, Optional
14
+
15
+ from dex.dex_api.models import SearchAttributeValueType
16
+
17
+ class PersistenceFieldType(Enum):
18
+ DataAttribute = 1
19
+ SearchAttribute = 2
20
+ DataAttributePrefix = 3
21
+
22
+ @dataclass
23
+ class PersistenceField:
24
+ key: str
25
+ field_type: PersistenceFieldType
26
+ value_type: Optional[type]
27
+ search_attribute_type: Optional[SearchAttributeValueType] = None
28
+
29
+ @classmethod
30
+ def data_attribute_def(cls, key: str, value_type: Optional[type]):
31
+ return PersistenceField(key, PersistenceFieldType.DataAttribute, value_type)
32
+
33
+ @classmethod
34
+ def search_attribute_def(cls, key: str, sa_type: SearchAttributeValueType):
35
+ return PersistenceField(
36
+ key, PersistenceFieldType.SearchAttribute, None, sa_type
37
+ )
38
+
39
+ @classmethod
40
+ def data_attribute_prefix_def(cls, key: str, value_type: Optional[type]):
41
+ return PersistenceField(
42
+ key, PersistenceFieldType.DataAttributePrefix, value_type
43
+ )
44
+
45
+ @dataclass
46
+ class PersistenceSchema:
47
+ persistence_fields: List[PersistenceField] = field(default_factory=list)
48
+
49
+ @classmethod
50
+ def create(cls, *args: PersistenceField):
51
+ return PersistenceSchema(list(args))
dex/py.typed ADDED
@@ -0,0 +1 @@
1
+ # Marker file for PEP 561
dex/registry.py ADDED
@@ -0,0 +1,204 @@
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 Callable, Optional
12
+
13
+ from dex.communication_schema import CommunicationMethodType
14
+ from dex.errors import InvalidArgumentError, WorkflowDefinitionError
15
+ from dex.dex_api.models import SearchAttributeValueType
16
+ from dex.persistence_options import PersistenceOptions
17
+ from dex.persistence_schema import PersistenceFieldType
18
+ from dex.rpc import RPCInfo
19
+ from dex.type_store import TypeStore, Type
20
+ from dex.workflow import ObjectWorkflow, get_workflow_type
21
+ from dex.workflow_state import WorkflowState, get_state_id
22
+
23
+ class Registry:
24
+ _workflow_store: dict[str, ObjectWorkflow]
25
+ _starting_state_store: dict[str, WorkflowState]
26
+ _state_store: dict[str, dict[str, WorkflowState]]
27
+ _internal_channel_type_store: dict[str, TypeStore]
28
+ _signal_channel_type_store: dict[str, dict[str, Optional[type]]]
29
+ _data_attribute_types: dict[str, TypeStore]
30
+ _search_attribute_types: dict[str, dict[str, SearchAttributeValueType]]
31
+ _persistence_options: dict[str, PersistenceOptions]
32
+ _rpc_infos: dict[str, dict[str, RPCInfo]]
33
+
34
+ def __init__(self):
35
+ self._workflow_store = dict()
36
+ self._starting_state_store = dict()
37
+ self._state_store = dict()
38
+ self._internal_channel_type_store = dict()
39
+ self._signal_channel_type_store = dict()
40
+ self._data_attribute_types = dict()
41
+ self._search_attribute_types = {}
42
+ self._persistence_options = {}
43
+ self._rpc_infos = dict()
44
+
45
+ def add_workflow(self, wf: ObjectWorkflow):
46
+ self._register_workflow_type(wf)
47
+ self._register_workflow_state(wf)
48
+ self._register_internal_channels(wf)
49
+ self._register_signal_channels(wf)
50
+ self._register_data_attributes(wf)
51
+ self._register_search_attributes(wf)
52
+ self._register_persistence_options(wf)
53
+ self._register_workflow_rpcs(wf)
54
+
55
+ def add_workflows(self, *wfs: ObjectWorkflow):
56
+ for wf in wfs:
57
+ self.add_workflow(wf)
58
+
59
+ def get_workflow_with_check(self, wf_type: str) -> ObjectWorkflow:
60
+ wf = self._workflow_store.get(wf_type)
61
+ if wf is None:
62
+ raise InvalidArgumentError(f"workflow {wf_type} is not registered")
63
+ return wf
64
+
65
+ def get_workflow_starting_state(self, wf_type: str) -> Optional[WorkflowState]:
66
+ return self._starting_state_store.get(wf_type)
67
+
68
+ def get_workflow_state_with_check(
69
+ self, wf_type: str, state_id: str
70
+ ) -> WorkflowState:
71
+ states = self._state_store.get(wf_type)
72
+ if states is None:
73
+ raise InvalidArgumentError(f"workflow {wf_type} is not registered")
74
+ state = states.get(state_id)
75
+ if state is None:
76
+ raise InvalidArgumentError(
77
+ f"workflow {wf_type} state {state_id} is not registered"
78
+ )
79
+ return state
80
+
81
+ def get_state_store(self, wf_type: str) -> dict[str, WorkflowState]:
82
+ return self._state_store[wf_type]
83
+
84
+ def get_internal_channel_type_store(self, wf_type: str) -> TypeStore:
85
+ return self._internal_channel_type_store[wf_type]
86
+
87
+ def get_signal_channel_types(self, wf_type: str) -> dict[str, Optional[type]]:
88
+ return self._signal_channel_type_store[wf_type]
89
+
90
+ def get_data_attribute_types(self, wf_type: str) -> TypeStore:
91
+ return self._data_attribute_types[wf_type]
92
+
93
+ def get_search_attribute_types(
94
+ self, wf_type: str
95
+ ) -> dict[str, SearchAttributeValueType]:
96
+ return self._search_attribute_types[wf_type]
97
+
98
+ def get_persistence_options(self, wf_type: str) -> PersistenceOptions:
99
+ return self._persistence_options[wf_type]
100
+
101
+ def get_rpc_infos(self, wf_type: str) -> dict[str, RPCInfo]:
102
+ return self._rpc_infos[wf_type]
103
+
104
+ def _register_workflow_type(self, wf: ObjectWorkflow):
105
+ wf_type = get_workflow_type(wf)
106
+ if wf_type in self._workflow_store:
107
+ raise WorkflowDefinitionError("workflow type conflict: ", wf_type)
108
+ self._workflow_store[wf_type] = wf
109
+
110
+ def _register_internal_channels(self, wf: ObjectWorkflow):
111
+ wf_type = get_workflow_type(wf)
112
+
113
+ if wf_type not in self._internal_channel_type_store:
114
+ self._internal_channel_type_store[wf_type] = TypeStore(
115
+ Type.INTERNAL_CHANNEL
116
+ )
117
+
118
+ for method in wf.get_communication_schema().communication_methods:
119
+ if method.method_type == CommunicationMethodType.InternalChannel:
120
+ self._internal_channel_type_store[wf_type].add_internal_channel_def(
121
+ method
122
+ )
123
+
124
+ def _register_signal_channels(self, wf: ObjectWorkflow):
125
+ wf_type = get_workflow_type(wf)
126
+ types: dict[str, Optional[type]] = {}
127
+ for method in wf.get_communication_schema().communication_methods:
128
+ if method.method_type == CommunicationMethodType.SignalChannel:
129
+ types[method.name] = method.value_type
130
+ self._signal_channel_type_store[wf_type] = types
131
+
132
+ def _register_data_attributes(self, wf: ObjectWorkflow):
133
+ wf_type = get_workflow_type(wf)
134
+ data_attribute_types: TypeStore = TypeStore(Type.DATA_ATTRIBUTE)
135
+ for field in wf.get_persistence_schema().persistence_fields:
136
+ if (
137
+ field.field_type == PersistenceFieldType.DataAttribute
138
+ or field.field_type == PersistenceFieldType.DataAttributePrefix
139
+ ):
140
+ data_attribute_types.add_data_attribute_def(field)
141
+ self._data_attribute_types[wf_type] = data_attribute_types
142
+
143
+ def _register_search_attributes(self, wf: ObjectWorkflow):
144
+ wf_type = get_workflow_type(wf)
145
+ types: dict[str, SearchAttributeValueType] = {}
146
+ for field in wf.get_persistence_schema().persistence_fields:
147
+ if field.field_type == PersistenceFieldType.SearchAttribute:
148
+ sa_type = field.search_attribute_type
149
+ if sa_type is None:
150
+ raise WorkflowDefinitionError(
151
+ f"Found search attribute {field.key} with no type set"
152
+ )
153
+ if field.key in types:
154
+ raise WorkflowDefinitionError(
155
+ f"Search attribute {field.key} already exists"
156
+ )
157
+ types[field.key] = sa_type
158
+ self._search_attribute_types[wf_type] = types
159
+
160
+ def _register_persistence_options(self, wf: ObjectWorkflow):
161
+ wf_type = get_workflow_type(wf)
162
+ self._persistence_options[wf_type] = wf.get_persistence_options()
163
+
164
+ def _register_workflow_state(self, wf):
165
+ wf_type = get_workflow_type(wf)
166
+ state_map = {}
167
+ starting_state = None
168
+ for state_def in wf.get_workflow_states().states:
169
+ state_id = get_state_id(state_def.state)
170
+ if state_id in state_map:
171
+ raise WorkflowDefinitionError(
172
+ f"Workflow {wf_type} cannot have duplicate stateId {state_id}"
173
+ )
174
+ state_map[state_id] = state_def.state
175
+ if state_def.can_start_workflow:
176
+ if starting_state is not None:
177
+ raise WorkflowDefinitionError(
178
+ f"Workflow {wf_type} cannot contain more than one starting "
179
+ f"state"
180
+ )
181
+ starting_state = state_def.state
182
+ self._state_store[wf_type] = state_map
183
+ self._starting_state_store[wf_type] = starting_state
184
+
185
+ @staticmethod
186
+ def _is_decorated_by_rpc(func: Callable):
187
+ return getattr(func, "_is_dex_rpc", False)
188
+
189
+ @staticmethod
190
+ def _get_rpc_info(func: Callable):
191
+ info = getattr(func, "_rpc_info")
192
+ assert isinstance(info, RPCInfo)
193
+ # NOTE: we have to override the method here so that it's associated the object
194
+ info.method_func = func
195
+ return info
196
+
197
+ def _register_workflow_rpcs(self, wf):
198
+ wf_type = get_workflow_type(wf)
199
+ rpc_infos = {}
200
+ for method_name in dir(wf):
201
+ method = getattr(wf, method_name)
202
+ if callable(method) and self._is_decorated_by_rpc(method):
203
+ rpc_infos[method_name] = self._get_rpc_info(method)
204
+ self._rpc_infos[wf_type] = rpc_infos
@@ -0,0 +1,67 @@
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
12
+ from typing import Optional
13
+
14
+ from dex.dex_api.models import WorkflowResetType
15
+
16
+ @dataclass
17
+ class ResetWorkflowTypeAndOptions:
18
+ reset_type: WorkflowResetType
19
+ reason: str
20
+ history_event_id: Optional[int] = None
21
+ history_event_time: Optional[str] = None
22
+ state_id: Optional[str] = None
23
+ state_execution_id: Optional[str] = None
24
+ skip_signal_reapply: Optional[bool] = None
25
+
26
+ def reset_to_beginning(reason: str) -> ResetWorkflowTypeAndOptions:
27
+ return ResetWorkflowTypeAndOptions(
28
+ reset_type=WorkflowResetType.BEGINNING,
29
+ reason=reason,
30
+ )
31
+
32
+ def reset_to_history_event_id(
33
+ history_event_id: int,
34
+ reason: str,
35
+ ) -> ResetWorkflowTypeAndOptions:
36
+ return ResetWorkflowTypeAndOptions(
37
+ reset_type=WorkflowResetType.HISTORY_EVENT_ID,
38
+ history_event_id=history_event_id,
39
+ reason=reason,
40
+ )
41
+
42
+ def reset_to_history_event_time(
43
+ history_event_time: str,
44
+ reason: str,
45
+ ) -> ResetWorkflowTypeAndOptions:
46
+ return ResetWorkflowTypeAndOptions(
47
+ reset_type=WorkflowResetType.HISTORY_EVENT_TIME,
48
+ history_event_time=history_event_time,
49
+ reason=reason,
50
+ )
51
+
52
+ def reset_to_state_id(state_id: str, reason: str) -> ResetWorkflowTypeAndOptions:
53
+ return ResetWorkflowTypeAndOptions(
54
+ reset_type=WorkflowResetType.STATE_ID,
55
+ state_id=state_id,
56
+ reason=reason,
57
+ )
58
+
59
+ def reset_to_state_execution_id(
60
+ state_execution: str,
61
+ reason: str,
62
+ ) -> ResetWorkflowTypeAndOptions:
63
+ return ResetWorkflowTypeAndOptions(
64
+ reset_type=WorkflowResetType.STATE_EXECUTION_ID,
65
+ state_execution_id=state_execution,
66
+ reason=reason,
67
+ )
dex/rpc.py ADDED
@@ -0,0 +1,93 @@
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
12
+ from functools import wraps
13
+ from inspect import signature
14
+ from typing import Callable, Optional
15
+
16
+ from dex.errors import WorkflowDefinitionError
17
+ from dex.dex_api.models import PersistenceLoadingPolicy, PersistenceLoadingType
18
+
19
+ @dataclass
20
+ class RPCInfo:
21
+ method_func: Callable
22
+ timeout_seconds: int
23
+ input_type: Optional[type] = None
24
+ data_attribute_loading_policy: Optional[PersistenceLoadingPolicy] = None
25
+ params_order: Optional[list] = (
26
+ None # store this so that the rpc can be invoked with correct parameters
27
+ )
28
+ bypass_caching_for_strong_consistency: bool = False
29
+
30
+ rpc_definition_err = WorkflowDefinitionError(
31
+ "an RPC must have at most 5 params: self, context:WorkflowContext, input:Any, persistence:Persistence, "
32
+ "communication:Communication, where input can be any type"
33
+ )
34
+
35
+ def rpc(
36
+ timeout_seconds: int = 10,
37
+ data_attribute_loading_policy: Optional[PersistenceLoadingPolicy] = None,
38
+ bypass_caching_for_strong_consistency: bool = False,
39
+ ):
40
+ def decorator(func):
41
+ # preserve the properties of the original function.
42
+ @wraps(func)
43
+ def wrapper(*args, **kwargs):
44
+ # TODO need to add type hint for decorated method
45
+ return func(*args, **kwargs)
46
+
47
+ wrapper._is_dex_rpc = True
48
+ rpc_info = RPCInfo(
49
+ method_func=func,
50
+ timeout_seconds=timeout_seconds,
51
+ data_attribute_loading_policy=data_attribute_loading_policy,
52
+ bypass_caching_for_strong_consistency=bypass_caching_for_strong_consistency,
53
+ )
54
+ params = signature(func).parameters
55
+
56
+ from inspect import _empty # ignored.
57
+ from dex.persistence import Persistence
58
+ from dex.workflow_context import WorkflowContext
59
+ from dex.communication import Communication
60
+
61
+ valid_param_types_exclude_input = {
62
+ _empty: True,
63
+ Persistence: True,
64
+ WorkflowContext: True,
65
+ Communication: True,
66
+ }
67
+ need_persistence = False
68
+ params_order = []
69
+ if len(params) > 5:
70
+ raise rpc_definition_err
71
+
72
+ has_input = False
73
+ for k, v in params.items():
74
+ if k != "self":
75
+ params_order.append(v.annotation)
76
+
77
+ if v.annotation == Persistence:
78
+ need_persistence = True
79
+ if v.annotation not in valid_param_types_exclude_input:
80
+ if not has_input:
81
+ has_input = True
82
+ rpc_info.input_type = v.annotation
83
+ else:
84
+ raise rpc_definition_err
85
+ if not need_persistence:
86
+ rpc_info.data_attribute_loading_policy = PersistenceLoadingPolicy(
87
+ persistence_loading_type=PersistenceLoadingType.LOAD_NONE
88
+ )
89
+ rpc_info.params_order = params_order
90
+ wrapper._rpc_info = rpc_info
91
+ return wrapper
92
+
93
+ return decorator
dex/runtime_errors.py ADDED
@@ -0,0 +1,81 @@
1
+ # Copyright (c) 2026 Super Durable, Inc.
2
+ #
3
+ # Licensed under the Super Durable Source License 1.0.
4
+ # You may not use this file except in compliance with the License.
5
+ # See the LICENSE file in the repository root.
6
+ #
7
+ # SPDX-License-Identifier: LicenseRef-Super-Durable-1.0
8
+
9
+ from __future__ import annotations
10
+
11
+ from enum import Enum
12
+ from typing import TYPE_CHECKING, Any
13
+
14
+ import grpc
15
+
16
+ from dex.flow_info import FlowStatus
17
+
18
+ if TYPE_CHECKING:
19
+ from dex._value_mapper import ValueMapper
20
+ from dex.dexpb import dex_pb2 as pb
21
+
22
+
23
+ class ErrorSubStatus(Enum):
24
+ UNCATEGORIZED = "uncategorized"
25
+ FLOW_ALREADY_STARTED = "flow_already_started"
26
+ FLOW_NOT_EXISTS = "flow_not_exists"
27
+ WORKER_API_ERROR = "worker_api_error"
28
+ LONG_POLL_TIMEOUT = "long_poll_timeout"
29
+
30
+
31
+ class FlowErrorType(Enum):
32
+ STEP_DECISION_FAILED = "step_decision_failed"
33
+ CLIENT_API_FAILED = "client_api_failed"
34
+ WORKER_API_FAILED = "worker_api_failed"
35
+ INVALID_USER_FLOW_CODE = "invalid_user_flow_code"
36
+ INTERNAL = "internal"
37
+
38
+
39
+ class DexException(RuntimeError):
40
+ def __init__(
41
+ self,
42
+ code: grpc.StatusCode,
43
+ sub_status: ErrorSubStatus | None,
44
+ detail: str | None,
45
+ worker_error_type: str = "",
46
+ worker_error_detail: str = "",
47
+ ) -> None:
48
+ super().__init__(detail)
49
+ self.code = code
50
+ self.sub_status = sub_status
51
+ self.detail = detail
52
+ self.worker_error_type = worker_error_type
53
+ self.worker_error_detail = worker_error_detail
54
+
55
+
56
+ class LongPollTimeoutError(RuntimeError):
57
+ def __init__(self, flow_id: str) -> None:
58
+ super().__init__(f"Flow is still running: {flow_id}")
59
+ self.flow_id = flow_id
60
+
61
+
62
+ class FlowUncompletedError(RuntimeError):
63
+ def __init__(
64
+ self,
65
+ run_id: str,
66
+ status: FlowStatus,
67
+ error_type: FlowErrorType | None,
68
+ message: str | None,
69
+ results: list[pb.StepCompletionOutput],
70
+ values: ValueMapper,
71
+ ) -> None:
72
+ super().__init__(message)
73
+ self.run_id = run_id
74
+ self.status = status
75
+ self.error_type = error_type
76
+ self.results = tuple(results)
77
+ self._values = values
78
+
79
+ def result(self, index: int, result_type: type[Any]) -> Any:
80
+ value = self.results[index].completed_step_output
81
+ return self._values.decode(value, self._values.codec(result_type))