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,85 @@
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 unittest
16
+
17
+ from dex.command_request import CommandRequest
18
+ from dex.command_results import CommandResults
19
+ from dex.communication import Communication
20
+ from dex.persistence import Persistence
21
+ from dex.state_decision import StateDecision
22
+ from dex.workflow_context import WorkflowContext
23
+ from dex.workflow_state import T, WorkflowState, should_skip_wait_until
24
+
25
+
26
+ class DirectStateSkip(WorkflowState[None]):
27
+ def execute(
28
+ self,
29
+ ctx: WorkflowContext,
30
+ input: T,
31
+ command_results: CommandResults,
32
+ persistence: Persistence,
33
+ communication: Communication,
34
+ ) -> StateDecision:
35
+ raise NotImplementedError
36
+
37
+
38
+ class DirectStateNotSkip(WorkflowState[int]):
39
+ def wait_until(
40
+ self,
41
+ ctx: WorkflowContext,
42
+ input: int,
43
+ persistence: Persistence,
44
+ communication: Communication,
45
+ ) -> CommandRequest:
46
+ raise NotImplementedError
47
+
48
+ def execute(
49
+ self,
50
+ ctx: WorkflowContext,
51
+ input: int,
52
+ command_results: CommandResults,
53
+ persistence: Persistence,
54
+ communication: Communication,
55
+ ) -> StateDecision:
56
+ raise NotImplementedError
57
+
58
+
59
+ class IndirectStateSkip(DirectStateSkip):
60
+ pass
61
+
62
+
63
+ class IndirectStateNotSkip(DirectStateSkip):
64
+ def wait_until(
65
+ self,
66
+ ctx: WorkflowContext,
67
+ input: T,
68
+ persistence: Persistence,
69
+ communication: Communication,
70
+ ) -> CommandRequest:
71
+ raise NotImplementedError
72
+
73
+
74
+ class TestSkipWaitUntil(unittest.TestCase):
75
+ def test_should_skip_wait_until(self) -> None:
76
+ direct_skip = DirectStateSkip()
77
+ direct_not_skip = DirectStateNotSkip()
78
+ indirect_skip = IndirectStateSkip()
79
+ indirect_not_skip = IndirectStateNotSkip()
80
+
81
+ assert should_skip_wait_until(direct_skip)
82
+ assert should_skip_wait_until(indirect_skip)
83
+
84
+ assert not should_skip_wait_until(direct_not_skip)
85
+ assert not should_skip_wait_until(indirect_not_skip)
@@ -0,0 +1,33 @@
1
+ # Copyright (c) 2022-2026 Super Durable, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import inspect
16
+ import time
17
+ import unittest
18
+
19
+ from dex.client import Client
20
+ from dex.tests.worker_server import registry
21
+ from dex.tests.workflows.recovery_workflow import RecoveryWorkflow
22
+
23
+
24
+ class Test(unittest.TestCase):
25
+ @classmethod
26
+ def setUpClass(cls):
27
+ cls.client = Client(registry)
28
+
29
+ def test_workflow_recovery(self):
30
+ wf_id = f"{inspect.currentframe().f_code.co_name}-{time.time_ns()}"
31
+ self.client.start_workflow(RecoveryWorkflow, wf_id, 10)
32
+ result = self.client.wait_for_workflow_completion(wf_id, str)
33
+ assert result == "done"
@@ -0,0 +1,40 @@
1
+ # Copyright (c) 2022-2026 Super Durable, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import inspect
16
+ import time
17
+ import unittest
18
+
19
+ from dex.client import Client
20
+ from dex.tests.worker_server import registry
21
+ from dex.tests.workflows.timer_workflow import TimerWorkflow, WaitState
22
+
23
+
24
+ class TestTimer(unittest.TestCase):
25
+ @classmethod
26
+ def setUpClass(cls):
27
+ cls.client = Client(registry)
28
+
29
+ def test_timer(self):
30
+ wf_id = f"{inspect.currentframe().f_code.co_name}-{time.time_ns()}"
31
+
32
+ self.client.start_workflow(TimerWorkflow, wf_id, 100, 5)
33
+ time.sleep(1)
34
+ self.client.skip_timer_at_command_index(wf_id, WaitState)
35
+ start_ms = time.time_ns() / 1000000
36
+ self.client.wait_for_workflow_completion(wf_id, None)
37
+ elapsed_ms = time.time_ns() / 1000000 - start_ms
38
+ assert (
39
+ 3000 <= elapsed_ms <= 6000
40
+ ), f"expected 5000 ms timer, actual is {elapsed_ms}"
@@ -0,0 +1,58 @@
1
+ # Copyright (c) 2022-2026 Super Durable, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import inspect
16
+ import time
17
+ import unittest
18
+
19
+ from dex.client import Client
20
+ from dex.tests.worker_server import registry
21
+ from dex.tests.workflows.wait_for_state_with_wait_for_key_workflow import (
22
+ WaitForStateWithWaitForKeyWorkflow,
23
+ WaitForStateWithWaitForKeyState2,
24
+ )
25
+ from dex.workflow_options import WorkflowOptions
26
+
27
+
28
+ class TestWaitForStateWithWaitForKey(unittest.TestCase):
29
+ @classmethod
30
+ def setUpClass(cls):
31
+ cls.client = Client(registry)
32
+
33
+ def test_wait_for_state_with_wait_for_key_workflow(self):
34
+ wf_id = f"{inspect.currentframe().f_code.co_name}-{time.time_ns()}"
35
+
36
+ wf_opts = WorkflowOptions()
37
+ wf_opts.add_wait_for_completion_state_ids(WaitForStateWithWaitForKeyState2)
38
+
39
+ self.client.start_workflow(
40
+ WaitForStateWithWaitForKeyWorkflow, wf_id, 100, None, wf_opts
41
+ )
42
+
43
+ self.client.wait_for_state_execution_completion_with_wait_for_key(
44
+ WaitForStateWithWaitForKeyState2, wf_id, "testKey"
45
+ )
46
+
47
+ self.client.wait_for_workflow_completion(wf_id)
48
+
49
+ # TODO: Improve this test by verifying both workflows completed with describe_workflow method
50
+ # https://github.com/superdurable/dex-python-sdk/issues/22
51
+ #
52
+ # child_wf_id =
53
+ #
54
+ # wf_info = self.client.describe_workflow(wf_id);
55
+ # assert wf_info.workflow_status == WorkflowStatus.COMPLETED
56
+ #
57
+ # child_wf_info = self.client.describe_workflow(child_wf_id);
58
+ # assert child_wf_info.workflow_status == WorkflowStatus.COMPLETED
@@ -0,0 +1,92 @@
1
+ # Copyright (c) 2022-2026 Super Durable, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import inspect
16
+ import time
17
+ import unittest
18
+
19
+ from dex.client import Client
20
+ from dex.errors import (
21
+ WorkflowAlreadyStartedError,
22
+ WorkflowCanceled,
23
+ WorkflowFailed,
24
+ WorkflowNotExistsError,
25
+ WorkflowStillRunningError,
26
+ WorkflowTerminated,
27
+ WorkflowTimeout,
28
+ )
29
+ from dex.dex_api.models import WorkflowStopType
30
+ from dex.stop_workflow_options import StopWorkflowOptions
31
+ from dex.tests.worker_server import registry
32
+ from dex.tests.workflows.wait_internal_channel_workflow import (
33
+ WaitInternalChannelWorkflow,
34
+ )
35
+
36
+
37
+ class TestWorkflowErrors(unittest.TestCase):
38
+ @classmethod
39
+ def setUpClass(cls):
40
+ cls.client = Client(registry)
41
+
42
+ def test_workflow_timeout(self):
43
+ wf_id = f"{inspect.currentframe().f_code.co_name}-{time.time_ns()}"
44
+ self.client.start_workflow(WaitInternalChannelWorkflow, wf_id, 1)
45
+ with self.assertRaises(WorkflowTimeout):
46
+ self.client.wait_for_workflow_completion(wf_id, str)
47
+ with self.assertRaises(WorkflowNotExistsError):
48
+ self.client.wait_for_workflow_completion("invalid_id", str)
49
+
50
+ def test_workflow_still_running_when_wait(self):
51
+ wf_id = f"{inspect.currentframe().f_code.co_name}-{time.time_ns()}"
52
+ # client_options = ClientOptions.local_default()
53
+ # client_options.api_timeout = 5
54
+ # TODO using a shorter api timeout will throw a different timeout error, it's better to unify it
55
+ self.client.start_workflow(WaitInternalChannelWorkflow, wf_id, 61)
56
+
57
+ with self.assertRaises(WorkflowAlreadyStartedError):
58
+ self.client.start_workflow(WaitInternalChannelWorkflow, wf_id, 61)
59
+
60
+ with self.assertRaises(WorkflowStillRunningError):
61
+ self.client.wait_for_workflow_completion(wf_id, str)
62
+
63
+ def test_workflow_canceled(self):
64
+ wf_id = f"{inspect.currentframe().f_code.co_name}-{time.time_ns()}"
65
+ self.client.start_workflow(WaitInternalChannelWorkflow, wf_id, 10)
66
+ self.client.stop_workflow(wf_id)
67
+ with self.assertRaises(WorkflowCanceled):
68
+ self.client.wait_for_workflow_completion(wf_id, str)
69
+
70
+ def test_workflow_terminated(self):
71
+ wf_id = f"{inspect.currentframe().f_code.co_name}-{time.time_ns()}"
72
+ self.client.start_workflow(WaitInternalChannelWorkflow, wf_id, 10)
73
+ self.client.stop_workflow(
74
+ wf_id,
75
+ StopWorkflowOptions(
76
+ workflow_stop_type=WorkflowStopType.TERMINATE, reason="test"
77
+ ),
78
+ )
79
+ with self.assertRaises(WorkflowTerminated):
80
+ self.client.wait_for_workflow_completion(wf_id, str)
81
+
82
+ def test_workflow_failed(self):
83
+ wf_id = f"{inspect.currentframe().f_code.co_name}-{time.time_ns()}"
84
+ self.client.start_workflow(WaitInternalChannelWorkflow, wf_id, 10)
85
+ self.client.stop_workflow(
86
+ wf_id,
87
+ StopWorkflowOptions(
88
+ workflow_stop_type=WorkflowStopType.FAIL, reason="test"
89
+ ),
90
+ )
91
+ with self.assertRaises(WorkflowFailed):
92
+ self.client.wait_for_workflow_completion(wf_id, str)
@@ -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
+ import inspect
16
+ import time
17
+ import unittest
18
+
19
+ from dex.client import Client
20
+ from dex.dex_api.models import IDReusePolicy
21
+ from dex.dex_api.models import (
22
+ PersistenceLoadingPolicy,
23
+ PersistenceLoadingType,
24
+ WorkflowStateOptions as IdlWorkflowStateOptions,
25
+ RetryPolicy,
26
+ WaitUntilApiFailurePolicy,
27
+ )
28
+ from dex.tests.worker_server import registry
29
+ from dex.tests.workflows.state_options_workflow import (
30
+ StateOptionsWorkflow1,
31
+ StateOptionsWorkflow2,
32
+ )
33
+ from dex.workflow_options import WorkflowOptions
34
+ from dex.workflow_state_options import WorkflowStateOptions, _to_idl_state_options
35
+ from ..errors import WorkflowFailed
36
+
37
+
38
+ class TestWorkflowStateOptions(unittest.TestCase):
39
+ @classmethod
40
+ def setUpClass(cls):
41
+ cls.client = Client(registry)
42
+
43
+ def test_convert_to_idl(self):
44
+ empty_idl = IdlWorkflowStateOptions()
45
+ assert empty_idl == _to_idl_state_options(False, None, {})
46
+
47
+ non_empty = WorkflowStateOptions(
48
+ "state-id",
49
+ search_attributes_loading_policy=PersistenceLoadingPolicy(
50
+ persistence_loading_type=PersistenceLoadingType.LOAD_ALL_WITHOUT_LOCKING
51
+ ),
52
+ )
53
+ non_empty_idl = IdlWorkflowStateOptions(
54
+ skip_wait_until=True,
55
+ search_attributes_loading_policy=PersistenceLoadingPolicy(
56
+ persistence_loading_type=PersistenceLoadingType.LOAD_ALL_WITHOUT_LOCKING
57
+ ),
58
+ )
59
+ assert non_empty_idl == _to_idl_state_options(True, non_empty, {})
60
+ non_empty.state_id = "state-id-2"
61
+ assert non_empty.state_id == "state-id-2"
62
+
63
+ """Test that proceed_to_execute_when_wait_until_retry_exhausted correctly handles both enum values."""
64
+
65
+ def test_proceed_to_execute_when_wait_until_retry_exhausted(self):
66
+ retry_policy = RetryPolicy(maximum_attempts=1)
67
+
68
+ # Test PROCEED_ON_FAILURE
69
+ options_proceed = WorkflowStateOptions(
70
+ proceed_to_execute_when_wait_until_retry_exhausted=WaitUntilApiFailurePolicy.PROCEED_ON_FAILURE,
71
+ wait_until_api_retry_policy=retry_policy,
72
+ )
73
+ result_proceed = _to_idl_state_options(False, options_proceed, {})
74
+ assert (
75
+ result_proceed.wait_until_api_failure_policy
76
+ == WaitUntilApiFailurePolicy.PROCEED_ON_FAILURE
77
+ )
78
+
79
+ # Test FAIL_WORKFLOW_ON_FAILURE
80
+ options_fail = WorkflowStateOptions(
81
+ proceed_to_execute_when_wait_until_retry_exhausted=WaitUntilApiFailurePolicy.FAIL_WORKFLOW_ON_FAILURE,
82
+ wait_until_api_retry_policy=retry_policy,
83
+ )
84
+ result_fail = _to_idl_state_options(False, options_fail, {})
85
+ assert (
86
+ result_fail.wait_until_api_failure_policy
87
+ == WaitUntilApiFailurePolicy.FAIL_WORKFLOW_ON_FAILURE
88
+ )
89
+
90
+ # Test with None/unset value
91
+ options = WorkflowStateOptions()
92
+ result = _to_idl_state_options(False, options, {})
93
+ # By default, wait_until_api_failure_policy should not be set when proceed_to_execute_when_wait_until_retry_exhausted is None
94
+ # The DEX service will use FAIL_WORKFLOW_ON_FAILURE by default
95
+ from dex.dex_api.types import Unset
96
+
97
+ self.assertTrue(isinstance(result.wait_until_api_failure_policy, Unset))
98
+
99
+ def test_proceed_on_failure(self):
100
+ wf_id = f"{inspect.currentframe().f_code.co_name}-{time.time_ns()}"
101
+ self.client.start_workflow(
102
+ StateOptionsWorkflow1,
103
+ wf_id,
104
+ 10,
105
+ "input",
106
+ WorkflowOptions(workflow_id_reuse_policy=IDReusePolicy.DISALLOW_REUSE),
107
+ )
108
+ output = self.client.wait_for_workflow_completion(wf_id)
109
+
110
+ assert output == "InitState1_execute_completed"
111
+
112
+ def test_fail_workflow_on_failure(self):
113
+ wf_id = f"{inspect.currentframe().f_code.co_name}-{time.time_ns()}"
114
+ self.client.start_workflow(
115
+ StateOptionsWorkflow2,
116
+ wf_id,
117
+ 10,
118
+ "input",
119
+ WorkflowOptions(workflow_id_reuse_policy=IDReusePolicy.DISALLOW_REUSE),
120
+ )
121
+
122
+ with self.assertRaises(WorkflowFailed):
123
+ self.client.wait_for_workflow_completion(wf_id, str)
@@ -0,0 +1,49 @@
1
+ # Copyright (c) 2022-2026 Super Durable, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import inspect
16
+ import time
17
+ import unittest
18
+
19
+ from dex.client import Client
20
+ from dex.workflow_options import WorkflowOptions
21
+
22
+ from dex.dex_api.models import IDReusePolicy
23
+ from dex.tests.worker_server import registry
24
+
25
+ from dex.tests.workflows.state_options_override_workflow import (
26
+ StateOptionsOverrideWorkflow,
27
+ )
28
+
29
+
30
+ class TestStateOptionsOverrideWorkflow(unittest.TestCase):
31
+ @classmethod
32
+ def setUpClass(cls):
33
+ cls.client = Client(registry)
34
+
35
+ def test_override(self):
36
+ wf_id = f"{inspect.currentframe().f_code.co_name}-{time.time_ns()}"
37
+ self.client.start_workflow(
38
+ StateOptionsOverrideWorkflow,
39
+ wf_id,
40
+ 10,
41
+ "input",
42
+ WorkflowOptions(workflow_id_reuse_policy=IDReusePolicy.DISALLOW_REUSE),
43
+ )
44
+ output = self.client.wait_for_workflow_completion(wf_id)
45
+
46
+ assert (
47
+ output
48
+ == "input_InitState_waitUntil_completed_InitState_execute_completed_NonInitState_execute_completed"
49
+ )
@@ -0,0 +1,74 @@
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 threading import Thread
16
+
17
+ from flask import Flask, request
18
+
19
+ from dex.dex_api.models import (
20
+ WorkflowStateExecuteRequest,
21
+ WorkflowStateWaitUntilRequest,
22
+ WorkflowWorkerRpcRequest,
23
+ )
24
+ from dex.tests import registry
25
+ from dex.worker_service import (
26
+ WorkerService,
27
+ )
28
+
29
+ # NOTE: set this to true when debugging(using breakpoints)
30
+ # so that it keep the thread running so that we can see the error in history
31
+ debug_mode: bool = False
32
+
33
+ _flask_app = Flask(__name__)
34
+
35
+ _worker_service = WorkerService(registry)
36
+
37
+
38
+ @_flask_app.route("/")
39
+ def index():
40
+ return "hello"
41
+
42
+
43
+ @_flask_app.route(WorkerService.api_path_workflow_state_wait_until, methods=["POST"])
44
+ def handle_wait_until():
45
+ req = WorkflowStateWaitUntilRequest.from_dict(request.json)
46
+ resp = _worker_service.handle_workflow_state_wait_until(req)
47
+ return resp.to_dict()
48
+
49
+
50
+ @_flask_app.route(WorkerService.api_path_workflow_state_execute, methods=["POST"])
51
+ def handle_execute():
52
+ req = WorkflowStateExecuteRequest.from_dict(request.json)
53
+ resp = _worker_service.handle_workflow_state_execute(req)
54
+ return resp.to_dict()
55
+
56
+
57
+ @_flask_app.route(WorkerService.api_path_workflow_worker_rpc, methods=["POST"])
58
+ def handle_rpc():
59
+ req = WorkflowWorkerRpcRequest.from_dict(request.json)
60
+ resp = _worker_service.handle_workflow_worker_rpc(req)
61
+ return resp.to_dict()
62
+
63
+
64
+ # this handler is extremely useful for debugging Dex
65
+ # the WebUI will be able to show you the error with stacktrace
66
+ @_flask_app.errorhandler(Exception)
67
+ def internal_error(exception):
68
+ return _worker_service.handle_worker_error(exception), 500
69
+
70
+
71
+ _webApp = Thread(target=_flask_app.run, args=("0.0.0.0", 8802))
72
+ # when debugging, keep the thread running so that we can see the error in history
73
+ _webApp.setDaemon(not debug_mode)
74
+ _webApp.start()
@@ -0,0 +1,48 @@
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.command_results import CommandResults
18
+ from dex.communication import Communication
19
+ from dex.dex_api.models import RetryPolicy
20
+ from dex.persistence import Persistence
21
+ from dex.state_decision import StateDecision
22
+ from dex.state_schema import StateSchema
23
+ from dex.workflow import ObjectWorkflow
24
+ from dex.workflow_context import WorkflowContext
25
+ from dex.workflow_state import T, WorkflowState
26
+ from dex.workflow_state_options import WorkflowStateOptions
27
+
28
+
29
+ class AbnormalExitState1(WorkflowState[Union[int, str]]):
30
+ def execute(
31
+ self,
32
+ ctx: WorkflowContext,
33
+ input: T,
34
+ command_results: CommandResults,
35
+ persistence: Persistence,
36
+ communication: Communication,
37
+ ) -> StateDecision:
38
+ raise RuntimeError("abnormal exit state")
39
+
40
+ def get_state_options(self) -> WorkflowStateOptions:
41
+ return WorkflowStateOptions(
42
+ execute_api_retry_policy=RetryPolicy(maximum_attempts=1)
43
+ )
44
+
45
+
46
+ class AbnormalExitWorkflow(ObjectWorkflow):
47
+ def get_workflow_states(self) -> StateSchema:
48
+ return StateSchema.with_starting_state(AbnormalExitState1())
@@ -0,0 +1,69 @@
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.command_request import CommandRequest, TimerCommand
18
+ from dex.command_results import CommandResults
19
+ from dex.communication import Communication
20
+ from dex.persistence import Persistence
21
+ from dex.state_decision import StateDecision
22
+ from dex.state_schema import StateSchema
23
+ from dex.workflow import ObjectWorkflow
24
+ from dex.workflow_context import WorkflowContext
25
+ from dex.workflow_state import T, WorkflowState
26
+
27
+
28
+ class State1(WorkflowState[Union[int, str]]):
29
+ def wait_until(
30
+ self,
31
+ ctx: WorkflowContext,
32
+ input: T,
33
+ persistence: Persistence,
34
+ communication: Communication,
35
+ ) -> CommandRequest:
36
+ if input != "input":
37
+ raise RuntimeError("input is incorrect")
38
+ return CommandRequest.for_all_command_completed(
39
+ TimerCommand.by_seconds(1),
40
+ )
41
+
42
+ def execute(
43
+ self,
44
+ ctx: WorkflowContext,
45
+ input: T,
46
+ command_results: CommandResults,
47
+ persistence: Persistence,
48
+ communication: Communication,
49
+ ) -> StateDecision:
50
+ if input != "input":
51
+ raise RuntimeError("input is incorrect")
52
+ return StateDecision.single_next_state(State2)
53
+
54
+
55
+ class State2(WorkflowState[None]):
56
+ def execute(
57
+ self,
58
+ ctx: WorkflowContext,
59
+ input: T,
60
+ command_results: CommandResults,
61
+ persistence: Persistence,
62
+ communication: Communication,
63
+ ) -> StateDecision:
64
+ return StateDecision.graceful_complete_workflow("done")
65
+
66
+
67
+ class BasicWorkflow(ObjectWorkflow):
68
+ def get_workflow_states(self) -> StateSchema:
69
+ return StateSchema.with_starting_state(State1(), State2())