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.
- dex/__init__.py +146 -0
- dex/_grpc_errors.py +73 -0
- dex/_invocation_context.py +269 -0
- dex/_native.pyd +0 -0
- dex/_native.pyi +20 -0
- dex/_utils.py +18 -0
- dex/_value_hydrator.py +223 -0
- dex/_value_mapper.py +174 -0
- dex/_worker_dispatcher.py +451 -0
- dex/_worker_service.py +60 -0
- dex/attribute.py +87 -0
- dex/blob_cache.py +73 -0
- dex/channel.py +165 -0
- dex/client.py +715 -0
- dex/client_options.py +19 -0
- dex/codec.py +286 -0
- dex/command_request.py +120 -0
- dex/command_results.py +107 -0
- dex/communication.py +136 -0
- dex/communication_schema.py +54 -0
- dex/condition.py +74 -0
- dex/context.py +86 -0
- dex/data_attributes.py +70 -0
- dex/dexpb/__init__.py +1 -0
- dex/dexpb/dex_pb2.py +381 -0
- dex/dexpb/dex_pb2.pyi +1734 -0
- dex/dexpb/dex_pb2_grpc.py +1298 -0
- dex/errors.py +109 -0
- dex/flow.py +456 -0
- dex/flow_config.py +29 -0
- dex/flow_info.py +51 -0
- dex/flow_options.py +122 -0
- dex/object_encoder.py +799 -0
- dex/persistence.py +89 -0
- dex/persistence_options.py +12 -0
- dex/persistence_schema.py +51 -0
- dex/py.typed +1 -0
- dex/registry.py +204 -0
- dex/reset_workflow_type_and_options.py +67 -0
- dex/rpc.py +93 -0
- dex/runtime_errors.py +81 -0
- dex/search_attributes.py +184 -0
- dex/state_decision.py +153 -0
- dex/state_execution_locals.py +66 -0
- dex/state_movement.py +115 -0
- dex/state_schema.py +48 -0
- dex/step.py +194 -0
- dex/step_execution.py +42 -0
- dex/stop_workflow_options.py +18 -0
- dex/tests/__init__.py +80 -0
- dex/tests/dex-service-env/.env +7 -0
- dex/tests/dex-service-env/docker-compose-init.sh +44 -0
- dex/tests/dex-service-env/docker-compose.yml +97 -0
- dex/tests/dex-service-env/dynamicconfig/README.md +39 -0
- dex/tests/dex-service-env/dynamicconfig/development-sql.yaml +9 -0
- dex/tests/dex-service-env/dynamicconfig/docker.yaml +2 -0
- dex/tests/test_abnormal_exit_workflow.py +43 -0
- dex/tests/test_basic_workflow.py +70 -0
- dex/tests/test_conditional_complete.py +50 -0
- dex/tests/test_describe_workflow.py +40 -0
- dex/tests/test_empty_data_decodes_properly.py +74 -0
- dex/tests/test_internal_channel.py +28 -0
- dex/tests/test_internal_channel_with_no_prefix_channel.py +41 -0
- dex/tests/test_persistence_data_attributes.py +62 -0
- dex/tests/test_persistence_search_attributes.py +127 -0
- dex/tests/test_persistence_state_execution_locals.py +38 -0
- dex/tests/test_rpc.py +64 -0
- dex/tests/test_rpc_with_memo.py +195 -0
- dex/tests/test_rpc_with_memo_duplicate_java_tests.py +117 -0
- dex/tests/test_signal.py +51 -0
- dex/tests/test_skip_wait_until.py +76 -0
- dex/tests/test_state_failure_recovery.py +28 -0
- dex/tests/test_timer.py +35 -0
- dex/tests/test_wait_for_state_execution_completion.py +53 -0
- dex/tests/test_workflow_errors.py +87 -0
- dex/tests/test_workflow_state_options.py +118 -0
- dex/tests/test_workflow_state_options_override.py +44 -0
- dex/tests/worker_server.py +64 -0
- dex/tests/workflows/abnormal_exit_workflow.py +42 -0
- dex/tests/workflows/basic_workflow.py +62 -0
- dex/tests/workflows/conditional_complete_workflow.py +95 -0
- dex/tests/workflows/describe_workflow.py +46 -0
- dex/tests/workflows/empty_data_workflow.py +45 -0
- dex/tests/workflows/internal_channel_workflow.py +129 -0
- dex/tests/workflows/internal_channel_workflow_with_no_prefix_channel.py +100 -0
- dex/tests/workflows/java_duplicate_rpc_memo_workflow.py +276 -0
- dex/tests/workflows/persistence_data_attributes_workflow.py +98 -0
- dex/tests/workflows/persistence_search_attributes_workflow.py +159 -0
- dex/tests/workflows/persistence_state_execution_local_workflow.py +63 -0
- dex/tests/workflows/recovery_workflow.py +82 -0
- dex/tests/workflows/rpc_memo_workflow.py +231 -0
- dex/tests/workflows/rpc_workflow.py +117 -0
- dex/tests/workflows/state_options_override_workflow.py +93 -0
- dex/tests/workflows/state_options_workflow.py +84 -0
- dex/tests/workflows/timer_workflow.py +46 -0
- dex/tests/workflows/wait_for_state_with_state_execution_id_workflow.py +70 -0
- dex/tests/workflows/wait_for_state_with_wait_for_key_workflow.py +71 -0
- dex/tests/workflows/wait_internal_channel_workflow.py +47 -0
- dex/tests/workflows/wait_signal_workflow.py +147 -0
- dex/timer.py +21 -0
- dex/type_store.py +99 -0
- dex/unregistered_client.py +585 -0
- dex/utils/__init__.py +3 -0
- dex/utils/dex_typing.py +25 -0
- dex/utils/persistence_utils.py +32 -0
- dex/wait.py +49 -0
- dex/worker.py +121 -0
- dex/worker_options.py +22 -0
- dex/worker_service.py +432 -0
- dex/workflow.py +79 -0
- dex/workflow_context.py +44 -0
- dex/workflow_info.py +16 -0
- dex/workflow_options.py +74 -0
- dex/workflow_state.py +123 -0
- dex/workflow_state_options.py +154 -0
- dex_python_sdk-0.0.2.dist-info/METADATA +202 -0
- dex_python_sdk-0.0.2.dist-info/RECORD +121 -0
- dex_python_sdk-0.0.2.dist-info/WHEEL +4 -0
- dex_python_sdk-0.0.2.dist-info/licenses/LEGACY_NOTICES.md +61 -0
- dex_python_sdk-0.0.2.dist-info/licenses/LICENSE +192 -0
- 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
|
dex/workflow_options.py
ADDED
|
@@ -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
|
+
|