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/workflow_info.py ADDED
@@ -0,0 +1,16 @@
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 dex.dex_api.models.workflow_status import WorkflowStatus
13
+
14
+ @dataclass
15
+ class WorkflowInfo:
16
+ workflow_status: WorkflowStatus
@@ -0,0 +1,74 @@
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 Any, Optional
13
+
14
+ from dex.dex_api.models import (
15
+ IDReusePolicy,
16
+ WorkflowRetryPolicy,
17
+ WorkflowAlreadyStartedOptions,
18
+ WorkflowConfig,
19
+ )
20
+ from dex.workflow_state import (
21
+ WorkflowState,
22
+ get_state_id_by_class,
23
+ get_state_execution_id,
24
+ )
25
+
26
+ @dataclass
27
+ class WorkflowOptions:
28
+ workflow_id_reuse_policy: Optional[IDReusePolicy] = None
29
+ workflow_cron_schedule: Optional[str] = None
30
+ workflow_start_delay_seconds: Optional[int] = None
31
+ workflow_retry_policy: Optional[WorkflowRetryPolicy] = None
32
+ workflow_already_started_options: Optional[WorkflowAlreadyStartedOptions] = None
33
+ workflow_config_override: Optional[WorkflowConfig] = None
34
+ initial_data_attributes: Optional[dict[str, Any]] = None
35
+ _wait_for_completion_state_ids: list[str] = field(default_factory=list)
36
+ _wait_for_completion_state_execution_ids: list[str] = field(default_factory=list)
37
+ initial_search_attributes: Optional[dict[str, Any]] = None
38
+
39
+ @property
40
+ def wait_for_completion_state_ids(self) -> Optional[list[str]]:
41
+ return self._wait_for_completion_state_ids
42
+
43
+ @wait_for_completion_state_ids.setter
44
+ def wait_for_completion_state_ids(self, *states: type[WorkflowState]):
45
+ state_ids: list[str] = []
46
+ for state in states:
47
+ state_ids.append(get_state_id_by_class(state))
48
+ self._wait_for_completion_state_ids = state_ids
49
+
50
+ def add_wait_for_completion_state_ids(self, *states: type[WorkflowState]):
51
+ for state in states:
52
+ self._wait_for_completion_state_ids.append(get_state_id_by_class(state))
53
+
54
+ @property
55
+ def wait_for_completion_state_execution_ids(self) -> Optional[list[str]]:
56
+ return self._wait_for_completion_state_execution_ids
57
+
58
+ @wait_for_completion_state_execution_ids.setter
59
+ def wait_for_completion_state_execution_ids(self, val):
60
+ try:
61
+ state, number = val
62
+ except ValueError:
63
+ raise ValueError(
64
+ "Pass an iterable with two items: state: type[WorkflowState] and number: int"
65
+ )
66
+ else:
67
+ state_id = get_state_execution_id(state, number)
68
+ self._wait_for_completion_state_execution_ids = state_id
69
+
70
+ def add_wait_for_completion_state_execution_id(
71
+ self, state: type[WorkflowState], number: int
72
+ ):
73
+ state_id = get_state_execution_id(state, number)
74
+ self._wait_for_completion_state_execution_ids.append(state_id)
dex/workflow_state.py ADDED
@@ -0,0 +1,123 @@
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 abc import ABC
12
+ from typing import Generic, TypeVar, get_args
13
+
14
+ from dex.command_request import CommandRequest
15
+ from dex.command_results import CommandResults
16
+ from dex.communication import Communication
17
+ from dex.persistence import Persistence
18
+ from dex.state_decision import StateDecision
19
+ from dex.workflow_context import WorkflowContext
20
+ from dex.workflow_state_options import WorkflowStateOptions
21
+
22
+ T = TypeVar("T")
23
+
24
+ not_implemented_error_msg = "This implementation shouldn't be invoked"
25
+
26
+ class WorkflowState(ABC, Generic[T]):
27
+ """WorkflowState is the interface to define a workflow state."""
28
+
29
+ def wait_until(
30
+ self,
31
+ ctx: WorkflowContext,
32
+ input: T,
33
+ persistence: Persistence,
34
+ communication: Communication,
35
+ ) -> CommandRequest:
36
+ """
37
+ WaitUntil is the method to set up commands set up to wait for, before `execute` API is invoked.
38
+ It's optional -- execute will be invoked instead if this is not implemented.
39
+
40
+ Args:
41
+ ctx: the context info of this API invocation, like workflow start time, workflowId, etc
42
+ input: input: the state input
43
+ persistence: the API for
44
+ 1) data attributes: defined by ObjectWorkflow interface
45
+ 2) search attributes: defined by ObjectWorkflow interface
46
+ 3) stateExecutionLocals: for passing data within the state execution
47
+ 4) recordEvent: for storing some tracking info(e.g. RPC call input/output) when executing the API.
48
+ Note that any write API will be recorded to server after the whole waitUntil API response is accepted
49
+ communication: the API right now only for publishing value to internalChannel
50
+ Note that any write API will be recorded to server after the whole waitUntil API response is accepted.
51
+
52
+ Returns: the requested command
53
+ """
54
+ raise NotImplementedError(not_implemented_error_msg)
55
+
56
+ def execute(
57
+ self,
58
+ ctx: WorkflowContext,
59
+ input: T,
60
+ command_results: CommandResults,
61
+ persistence: Persistence,
62
+ communication: Communication,
63
+ ) -> StateDecision:
64
+ """
65
+ Execute is the method to execute and decide what to do next. Invoke after commands from WaitUntil are completed, or there is WaitUntil is not implemented for the state.
66
+
67
+ Args:
68
+ ctx: the context info of this API invocation, like workflow start time, workflowId, etc
69
+ input: the state input
70
+ command_results: the results of the command that executed by WaitUntil
71
+ persistence: the API for
72
+ 1) data attributes: defined by ObjectWorkflow interface
73
+ 2) search attributes: defined by ObjectWorkflow interface
74
+ 3) stateExecutionLocals: for passing data within the state execution
75
+ 4) recordEvent: for storing some tracking info(e.g. RPC call input/output) when executing the API.
76
+ Note that any write API will be recorded to server after the whole waitUntil API response is accepted
77
+ communication: the API right now only for publishing value to internalChannel.
78
+ Note that any write API will be recorded to server after the whole execute API response is accepted.
79
+
80
+ Returns: the decision of what to do next(e.g. transition to next states or closing workflow)
81
+ """
82
+ raise NotImplementedError(not_implemented_error_msg)
83
+
84
+ def get_state_options(self) -> WorkflowStateOptions:
85
+ """GetStateOptions can just return nil to use the default Options
86
+ StateOptions is optional configuration to adjust the state behaviors. Default values:
87
+ StateId: name of the implementation class
88
+ waitUntilApiFailurePolicy: FAIL_WORKFLOW_ON_FAILURE
89
+ PersistenceLoadingPolicy for dataAttributes/searchAttributes: LOAD_ALL_WITHOUT_LOCKING
90
+ waitUntil/execute API:
91
+ timeout: 30s
92
+ retryPolicy:
93
+ InitialIntervalSeconds: 1
94
+ MaxInternalSeconds:100
95
+ MaximumAttempts: 0
96
+ BackoffCoefficient: 2
97
+ Returns: WorkflowStateOptions
98
+ """
99
+ return WorkflowStateOptions()
100
+
101
+ def get_state_id(state: WorkflowState) -> str:
102
+ options = state.get_state_options()
103
+ if options is None or options.state_id is None:
104
+ return state.__class__.__name__
105
+ return options.state_id
106
+
107
+ def get_state_id_by_class(state: type[WorkflowState]) -> str:
108
+ return state.__name__
109
+
110
+ def should_skip_wait_until(state: WorkflowState) -> bool:
111
+ func_name = state.wait_until.__name__
112
+ parent_method = getattr(super(type(state), state), func_name)
113
+ return parent_method == state.wait_until
114
+
115
+ def get_input_type(state):
116
+ bases = state.__orig_bases__
117
+ for b in bases:
118
+ if b.__origin__ == WorkflowState:
119
+ return get_args(b)[0]
120
+ return None
121
+
122
+ def get_state_execution_id(state: type[WorkflowState], number: int):
123
+ return f"{get_state_id_by_class(state)}-{number}"
@@ -0,0 +1,154 @@
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 Any, Optional
13
+
14
+ from dex.errors import WorkflowDefinitionError
15
+ from dex.dex_api.models import (
16
+ ExecuteApiFailurePolicy,
17
+ PersistenceLoadingPolicy,
18
+ RetryPolicy,
19
+ WaitUntilApiFailurePolicy,
20
+ WorkflowStateOptions as IdlWorkflowStateOptions,
21
+ )
22
+ from dex.dex_api.types import Unset
23
+
24
+ @dataclass
25
+ class WorkflowStateOptions:
26
+ state_id: Optional[str] = None
27
+ # apply for both waitUntil and execute API
28
+ data_attributes_loading_policy: Optional[PersistenceLoadingPolicy] = None
29
+ search_attributes_loading_policy: Optional[PersistenceLoadingPolicy] = None
30
+ # below are wait_until API specific options:
31
+ wait_until_api_timeout_seconds: Optional[int] = None
32
+ wait_until_api_retry_policy: Optional[RetryPolicy] = None
33
+ """
34
+ By default, workflow would fail after waitUntil API retry exhausted.
35
+ This policy to allow proceeding to the execute API after waitUntil API exhausted all retries.
36
+ This is useful for some advanced use cases like SAGA pattern.
37
+ RetryPolicy is required to be set with maximumAttempts or maximumAttemptsDurationSeconds for waitUntil API.
38
+ NOTE: execute API will use commandResults to check whether the waitUntil has succeeded or not.
39
+ See more in <a href="https://github.com/superdurable/dex/wiki/WorkflowStateOptions">wiki</a>
40
+ """
41
+ proceed_to_execute_when_wait_until_retry_exhausted: Optional[
42
+ WaitUntilApiFailurePolicy
43
+ ] = None
44
+ wait_until_api_data_attributes_loading_policy: Optional[
45
+ PersistenceLoadingPolicy
46
+ ] = None
47
+ wait_until_api_search_attributes_loading_policy: Optional[
48
+ PersistenceLoadingPolicy
49
+ ] = None
50
+ # below are execute API specific options:
51
+ execute_api_timeout_seconds: Optional[int] = None
52
+ execute_api_retry_policy: Optional[RetryPolicy] = None
53
+ """
54
+ By default, workflow would fail after execute API retry exhausted.
55
+ Set the state to proceed to the specified state after the execute API exhausted all retries
56
+ This is useful for some advanced use cases like SAGA pattern.
57
+ RetryPolicy is required to be set with maximumAttempts or maximumAttemptsDurationSeconds for execute API.
58
+ Note that the failure handling state will take the same input as the failed from state.
59
+ TODO the type should be the type is Optional[type[WorkflowState]] but -- there is an issue with circular import...
60
+ """
61
+ proceed_to_state_when_execute_retry_exhausted: Optional[type] = None
62
+ execute_api_data_attributes_loading_policy: Optional[PersistenceLoadingPolicy] = (
63
+ None
64
+ )
65
+ execute_api_search_attributes_loading_policy: Optional[PersistenceLoadingPolicy] = (
66
+ None
67
+ )
68
+
69
+ def _to_idl_state_options(
70
+ skip_wait_until: bool,
71
+ options: Any, # TODO this type was Optional[WorkflowStateOptions],
72
+ # however, type hint is not working with recursive call...
73
+ state_store: dict[str, Any], # TODO this type should be dict[str, WorkflowState]
74
+ ) -> IdlWorkflowStateOptions:
75
+ res = IdlWorkflowStateOptions()
76
+ if skip_wait_until:
77
+ res.skip_wait_until = True
78
+
79
+ if options is None:
80
+ return res
81
+ assert isinstance(options, WorkflowStateOptions)
82
+
83
+ if options.wait_until_api_search_attributes_loading_policy is not None:
84
+ res.wait_until_api_search_attributes_loading_policy = (
85
+ options.wait_until_api_search_attributes_loading_policy
86
+ )
87
+ if options.execute_api_search_attributes_loading_policy is not None:
88
+ res.execute_api_search_attributes_loading_policy = (
89
+ options.execute_api_search_attributes_loading_policy
90
+ )
91
+ if options.search_attributes_loading_policy is not None:
92
+ res.search_attributes_loading_policy = options.search_attributes_loading_policy
93
+ if options.wait_until_api_data_attributes_loading_policy is not None:
94
+ res.wait_until_api_data_attributes_loading_policy = (
95
+ options.wait_until_api_data_attributes_loading_policy
96
+ )
97
+ if options.execute_api_data_attributes_loading_policy is not None:
98
+ res.execute_api_data_attributes_loading_policy = (
99
+ options.execute_api_data_attributes_loading_policy
100
+ )
101
+ if options.data_attributes_loading_policy is not None:
102
+ res.data_attributes_loading_policy = options.data_attributes_loading_policy
103
+ if options.proceed_to_execute_when_wait_until_retry_exhausted is not None:
104
+ res.wait_until_api_failure_policy = (
105
+ options.proceed_to_execute_when_wait_until_retry_exhausted
106
+ )
107
+ if options.wait_until_api_retry_policy is None:
108
+ raise WorkflowDefinitionError("wait_until API retry policy must be set")
109
+ if isinstance(
110
+ options.wait_until_api_retry_policy.maximum_attempts, Unset
111
+ ) and isinstance(
112
+ options.wait_until_api_retry_policy.maximum_attempts_duration_seconds, Unset
113
+ ):
114
+ raise WorkflowDefinitionError(
115
+ "wait_until API retry policy must be set with maximum_attempts or maximum_attempts_duration_seconds"
116
+ )
117
+ if options.wait_until_api_retry_policy is not None:
118
+ res.wait_until_api_retry_policy = options.wait_until_api_retry_policy
119
+ if options.wait_until_api_timeout_seconds is not None:
120
+ res.wait_until_api_timeout_seconds = options.wait_until_api_timeout_seconds
121
+ if options.execute_api_retry_policy is not None:
122
+ res.execute_api_retry_policy = options.execute_api_retry_policy
123
+ if options.execute_api_timeout_seconds is not None:
124
+ res.execute_api_timeout_seconds = options.execute_api_timeout_seconds
125
+ if options.proceed_to_state_when_execute_retry_exhausted is not None:
126
+ res.execute_api_failure_policy = (
127
+ ExecuteApiFailurePolicy.PROCEED_TO_CONFIGURED_STATE
128
+ )
129
+ if options.execute_api_retry_policy is None:
130
+ raise WorkflowDefinitionError("execute API retry policy must be set")
131
+ if isinstance(
132
+ options.execute_api_retry_policy.maximum_attempts, Unset
133
+ ) and isinstance(
134
+ options.execute_api_retry_policy.maximum_attempts_duration_seconds, Unset
135
+ ):
136
+ raise WorkflowDefinitionError(
137
+ "execute API retry policy must be set with maximum_attempts or maximum_attempts_duration_seconds"
138
+ )
139
+
140
+ from dex.workflow_state import get_state_id_by_class
141
+
142
+ res.execute_api_failure_proceed_state_id = get_state_id_by_class(
143
+ options.proceed_to_state_when_execute_retry_exhausted
144
+ )
145
+ state = state_store[res.execute_api_failure_proceed_state_id]
146
+ proceed_state_options = state.get_state_options()
147
+
148
+ from dex.workflow_state import should_skip_wait_until
149
+
150
+ proceed_state_idl_options = _to_idl_state_options(
151
+ should_skip_wait_until(state), proceed_state_options, state_store
152
+ )
153
+ res.execute_api_failure_proceed_state_options = proceed_state_idl_options
154
+ return res
@@ -0,0 +1,202 @@
1
+ Metadata-Version: 2.4
2
+ Name: dex-python-sdk
3
+ Version: 0.0.2
4
+ Requires-Dist: grpcio>=1.83.0
5
+ Requires-Dist: grpcio-status>=1.83.0
6
+ Requires-Dist: httpx==0.28.1
7
+ Requires-Dist: protobuf>=7.35.1
8
+ License-File: LICENSE
9
+ License-File: LEGACY_NOTICES.md
10
+ Summary: Python SDK for the Dex workflow engine
11
+ Author: Super Durable
12
+ License-Expression: LicenseRef-Super-Durable-1.0
13
+ Requires-Python: >=3.11
14
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
15
+ Project-URL: Homepage, https://github.com/superdurable/dex/tree/main/sdk-python
16
+ Project-URL: Repository, https://github.com/superdurable/dex
17
+
18
+
19
+ # Dex SDK for Python
20
+
21
+ Python SDK for [Dex workflow engine](https://github.com/superdurable/dex)
22
+
23
+ ## New user contracts
24
+
25
+ The rewrite targets Python 3.11+ and exposes strongly typed workflow contracts
26
+ from `dex`. This phase includes definitions, attributes, channels, waits,
27
+ decisions, codecs, registry validation, synchronous client calls, and synchronous
28
+ worker handlers. Python owns its gRPC Client and Worker transport;
29
+ the shared Rust Core is used only for BlobCache.
30
+
31
+ ```python
32
+ from datetime import timedelta
33
+
34
+ import dex
35
+
36
+ counter = dex.Attribute("counter", int)
37
+ counters_by_region = dex.AttributeMap("counters-by-region", int)
38
+
39
+ class Run(dex.Step[str]):
40
+ def wait_for(
41
+ self, context: dex.Context, input: str
42
+ ) -> dex.Wait:
43
+ return dex.Wait.all_of(
44
+ dex.Timer.by_duration(timedelta(seconds=1))
45
+ )
46
+
47
+ def execute(
48
+ self, context: dex.Context, input: str
49
+ ) -> dex.StepDecision:
50
+ return dex.graceful_complete(input)
51
+
52
+ class CounterFlow(dex.Flow[str]):
53
+ run = Run()
54
+
55
+ def get_flow_type(self) -> str:
56
+ return "Counter"
57
+
58
+ def get_steps(self) -> dex.StepList[str]:
59
+ return dex.StepList.start_step(self.run)
60
+
61
+ def get_persistence_schema(self) -> dex.PersistenceSchema:
62
+ return dex.PersistenceSchema.of(counter, counters_by_region)
63
+
64
+ @dex.rpc(name="Increment")
65
+ def increment(
66
+ self, context: dex.Context, input: int
67
+ ) -> dex.RPCResult[int]:
68
+ return dex.RPCResult(input + 1)
69
+
70
+ flow = CounterFlow()
71
+ registry = dex.Registry((flow,))
72
+ ```
73
+
74
+ Registry derives codecs from declared Python types and handler annotations.
75
+ Built-in scalar types and dataclasses need no codec arguments. Register an
76
+ explicit codec only for a custom encoding or a type Registry cannot derive.
77
+ `PersistenceSchema.of(...)` accepts attributes and channels together and
78
+ partitions them by definition type.
79
+
80
+ Initial attributes retain their value types without a public wrapper class:
81
+
82
+ ```python
83
+ options = (
84
+ dex.StartFlowOptions()
85
+ .with_attribute(counter, 1)
86
+ .with_attribute(counters_by_region, "us-west", 1)
87
+ )
88
+ ```
89
+
90
+ ```
91
+ pip install dex-python-sdk==0.0.2
92
+ ```
93
+
94
+ See [samples](../examples/python) for use case examples.
95
+
96
+ ## Requirements
97
+
98
+ - Python 3.11+
99
+ - [Dex server](https://github.com/superdurable/dex#how-to-use)
100
+
101
+ ## Concepts
102
+
103
+ Applications implement two generic interfaces from [`dex.contracts`](dex/contracts/):
104
+
105
+ - `Flow[START_INPUT]` returns `StepList.start_step(...)`, followed by optional
106
+ `.other_steps(...)`, from one `get_steps()` method. The `StepList` generic
107
+ binds the Flow input to the starting Step input. Use `StepList.empty()` when
108
+ a Flow has no Steps.
109
+ - `Step[INPUT]` implements synchronous `execute` and optionally synchronous
110
+ `wait_for`.
111
+
112
+ `StepOptions.wait_for_method_timeout` and `execute_method_timeout` bound the
113
+ two handler calls. Timer and channel conditions determine how long a Step waits.
114
+
115
+ `Registry` validates every Flow, Step, RPC signature, durable name, lock, and
116
+ codec before Client or Worker startup. `Client` methods use these typed objects
117
+ instead of raw Flow, Step, or RPC strings.
118
+
119
+ The legacy IWF integration inventory is ported under
120
+ [`tests/integ`](tests/integ/README.md). Its 58 executable scenarios
121
+ exercise the same workflows, client operations, and assertions as the Java
122
+ suite against an isolated `dexcli dev` environment.
123
+
124
+ ## Implementation status
125
+
126
+ The strongly typed contracts, registry, synchronous Client, Worker gRPC
127
+ service, and Rust-backed BlobCache are implemented. Python owns its gRPC
128
+ transport; the native bridge is limited to the shared BlobCache.
129
+
130
+ ## Running dex-server locally
131
+
132
+ ### Option 1: use docker compose
133
+ See [dex README](https://github.com/superdurable/dex#using-docker-image--docker-compose)
134
+
135
+ ### Option 2: VSCode Dev Container
136
+
137
+ Dev Container is an easy way to get dex-server running locally. Follow these steps to launch a dev container:
138
+ - Install Docker, VSCode, and [VSCode Dev Container plugin](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers).
139
+ - Open the project in VSCode.
140
+ ```bash
141
+ cd dex-python-sdk
142
+ code .
143
+ ```
144
+ - Launch the Remote-Containers: Reopen in Container command from Command Palette (Ctrl + Shift + P). You can also click in the bottom left corner to access the remote container menu.
145
+ - Once the dev container starts, dex-server will be listening on port 8801.
146
+
147
+ ## How To Contribute
148
+
149
+ This project uses [uv](https://docs.astral.sh/uv/) for Python versions,
150
+ dependencies, virtual environments, locking, building, and publishing.
151
+
152
+ To install requirements:
153
+
154
+ ```bash
155
+ uv sync --locked
156
+ ```
157
+
158
+ Run the complete Python SDK integration suite with an isolated Dex development
159
+ environment:
160
+
161
+ ```bash
162
+ ./run-integration-tests.sh
163
+ ```
164
+
165
+ #### Update IDL
166
+
167
+ Edit [`protos/dex.proto`](../protos/dex.proto). Rename catalog: [`docs/design/idl-renames.md`](../docs/design/idl-renames.md).
168
+
169
+ #### Generate stubs from IDL
170
+
171
+ ```bash
172
+ make -C ../protos proto-python
173
+ ```
174
+
175
+ Checked-in Python stubs land in `dex/dexpb/`.
176
+ #### Linting
177
+
178
+ To run linting for this project:
179
+
180
+ ```bash
181
+ uv run --frozen pre-commit run --show-diff-on-failure --color=always --all-files
182
+ ```
183
+
184
+ ## Code of Conduct
185
+ This project is governed by the [Contributor Covenant v 1.4.1](CODE_OF_CONDUCT.md). (Review the Code of Conduct and remove this sentence before publishing your project.)
186
+
187
+ ## Publishing to PyPI
188
+
189
+ 1. Bump `version` in `pyproject.toml`, refresh `uv.lock`, and update the `pip install` line above.
190
+ 2. Run **Publish Python SDK to PyPI** manually without `publish` to validate all distributions.
191
+ 3. Create a GitHub Release with tag `sdk-python/vX.Y.Z` (for example `sdk-python/v0.0.2`).
192
+ 4. CI builds and smoke-tests Linux x86_64/ARM64, macOS x86_64/ARM64, and Windows x86_64 wheels, verifies the source distribution, and publishes them with `PYPI_TOKEN`.
193
+
194
+ A manual run publishes only from `main`, when its version matches `pyproject.toml` and `publish` is explicitly selected.
195
+
196
+ See [CONTRIBUTING.md](../CONTRIBUTING.md#releases-monorepo-tags) for monorepo tag conventions.
197
+
198
+ ## License
199
+
200
+ [Super Durable Source License 1.0](LICENSE), with legacy portions under their
201
+ original terms as described in [LEGACY_NOTICES.md](LEGACY_NOTICES.md).
202
+