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/errors.py ADDED
@@ -0,0 +1,109 @@
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
+ import json as jsonlib
12
+ from httpx import Response
13
+
14
+ from dex.dex_api.models import (
15
+ ErrorResponse,
16
+ ErrorSubStatus,
17
+ WorkflowGetResponse,
18
+ WorkflowStatus,
19
+ )
20
+
21
+ class WorkflowDefinitionError(Exception):
22
+ pass
23
+
24
+ class InvalidArgumentError(Exception):
25
+ pass
26
+
27
+ class NotRegisteredError(Exception):
28
+ pass
29
+
30
+ class HttpError(RuntimeError):
31
+ def __init__(self, status: int, err_resp: ErrorResponse):
32
+ super().__init__(err_resp.detail)
33
+ self.sub_status = err_resp.sub_status
34
+ self.error_resp = err_resp
35
+ self.status = status
36
+
37
+ class ClientSideError(HttpError):
38
+ pass
39
+
40
+ class ServerSideError(HttpError):
41
+ pass
42
+
43
+ class WorkflowStillRunningError(ClientSideError):
44
+ pass
45
+
46
+ class WorkflowRPCExecutionError(ClientSideError):
47
+ pass
48
+
49
+ class WorkflowRPCAcquiringLockFailure(ClientSideError):
50
+ pass
51
+
52
+ class WorkflowAlreadyStartedError(ClientSideError):
53
+ pass
54
+
55
+ class WorkflowNotExistsError(ClientSideError):
56
+ pass
57
+
58
+ def process_http_error(status: int, err_resp: ErrorResponse) -> HttpError:
59
+ if 400 <= status < 500:
60
+ if err_resp.sub_status == ErrorSubStatus.WORKFLOW_ALREADY_STARTED_SUB_STATUS:
61
+ return WorkflowAlreadyStartedError(status, err_resp)
62
+ elif err_resp.sub_status == ErrorSubStatus.WORKFLOW_NOT_EXISTS_SUB_STATUS:
63
+ return WorkflowNotExistsError(status, err_resp)
64
+ else:
65
+ return ClientSideError(status, err_resp)
66
+ else:
67
+ return ServerSideError(status, err_resp)
68
+
69
+ class WorkflowAbnormalExitError(RuntimeError):
70
+ def __init__(self, get_response: WorkflowGetResponse):
71
+ self.run_id = get_response.workflow_run_id
72
+ self.workflow_status = get_response.workflow_status
73
+ self.error_type = get_response.error_type
74
+ self.error_message = get_response.error_message
75
+ # TODO add methods to decode the state results into objects
76
+ self._state_results = get_response.results
77
+
78
+ class WorkflowFailed(WorkflowAbnormalExitError):
79
+ pass
80
+
81
+ class WorkflowTimeout(WorkflowAbnormalExitError):
82
+ pass
83
+
84
+ class WorkflowTerminated(WorkflowAbnormalExitError):
85
+ pass
86
+
87
+ class WorkflowCanceled(WorkflowAbnormalExitError):
88
+ pass
89
+
90
+ def process_workflow_abnormal_exit_error(
91
+ get_response: WorkflowGetResponse,
92
+ ) -> WorkflowAbnormalExitError:
93
+ status = get_response.workflow_status
94
+ if status == WorkflowStatus.CANCELED:
95
+ return WorkflowCanceled(get_response)
96
+ elif status == WorkflowStatus.FAILED:
97
+ return WorkflowFailed(get_response)
98
+ elif status == WorkflowStatus.TERMINATED:
99
+ return WorkflowTerminated(get_response)
100
+ elif status == WorkflowStatus.TIMEOUT:
101
+ return WorkflowTimeout(get_response)
102
+ return WorkflowAbnormalExitError(get_response)
103
+
104
+ def parse_unexpected_error(err) -> ErrorResponse:
105
+ try:
106
+ response = Response(err.status_code, content=err.content)
107
+ return ErrorResponse.from_dict(response.json())
108
+ except Exception:
109
+ return ErrorResponse.from_dict(jsonlib.loads(err.content))
dex/flow.py ADDED
@@ -0,0 +1,456 @@
1
+ # Legacy Materials in this file remain under their original licenses.
2
+ # See LEGACY_NOTICES.md.
3
+
4
+ # Modifications Copyright (c) 2026 Super Durable, Inc.
5
+ #
6
+ # Modifications after the Legacy Cutoff are licensed under the
7
+ # Super Durable Source License 1.0.
8
+ # Legacy Materials remain under their original licenses.
9
+ # See LICENSE and LEGACY_NOTICES.md.
10
+
11
+ from __future__ import annotations
12
+
13
+ from abc import ABC
14
+ from dataclasses import dataclass
15
+ from datetime import timedelta
16
+ from inspect import iscoroutinefunction, signature
17
+ from types import MappingProxyType
18
+ from typing import (
19
+ Any,
20
+ Callable,
21
+ Generic,
22
+ Sequence,
23
+ TypeVar,
24
+ get_args,
25
+ get_origin,
26
+ get_type_hints,
27
+ overload,
28
+ )
29
+ from urllib.parse import quote
30
+
31
+ from dex._utils import require_name
32
+ from dex.attribute import Attribute, AttributeLock, AttributeMap
33
+ from dex.channel import Channel, ChannelMap
34
+ from dex.codec import Codec, CodecRegistry
35
+ from dex.context import Context
36
+ from dex.step import Step, StepDecision, StepList, StepMovement, _StepDef
37
+ from dex.wait import Wait
38
+
39
+ OutputT = TypeVar("OutputT")
40
+ StartT = TypeVar("StartT")
41
+ CallableT = TypeVar("CallableT", bound=Callable[..., Any])
42
+ _PersistenceDefinition = (
43
+ Attribute[Any] | AttributeMap[Any] | Channel[Any] | ChannelMap[Any]
44
+ )
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class _RPCOptions:
49
+ name: str | None
50
+ timeout: timedelta | None
51
+ lock_attributes: tuple[AttributeLock, ...]
52
+
53
+
54
+ @overload
55
+ def rpc(handler: CallableT) -> CallableT: ...
56
+
57
+
58
+ @overload
59
+ def rpc(
60
+ *,
61
+ name: str | None = None,
62
+ timeout: timedelta | None = None,
63
+ lock_attributes: Sequence[AttributeLock] = (),
64
+ ) -> Callable[[CallableT], CallableT]: ...
65
+
66
+
67
+ def rpc(
68
+ handler: CallableT | None = None,
69
+ *,
70
+ name: str | None = None,
71
+ timeout: timedelta | None = None,
72
+ lock_attributes: Sequence[AttributeLock] = (),
73
+ ) -> CallableT | Callable[[CallableT], CallableT]:
74
+ if name is not None:
75
+ require_name(name)
76
+ if timeout is not None and timeout < timedelta(0):
77
+ raise ValueError("RPC timeout must not be negative")
78
+
79
+ def decorate(handler: CallableT) -> CallableT:
80
+ setattr(
81
+ handler,
82
+ "__dex_rpc_options__",
83
+ _RPCOptions(name, timeout, tuple(lock_attributes)),
84
+ )
85
+ return handler
86
+
87
+ if handler is not None:
88
+ return decorate(handler)
89
+ return decorate
90
+
91
+
92
+ @dataclass(frozen=True)
93
+ class RPCResult(Generic[OutputT]):
94
+ output: OutputT
95
+ next_steps: tuple[StepMovement[Any], ...] = ()
96
+
97
+
98
+ @dataclass(frozen=True)
99
+ class PersistenceSchema:
100
+ attributes: tuple[Attribute[Any] | AttributeMap[Any], ...] = ()
101
+ channels: tuple[Channel[Any] | ChannelMap[Any], ...] = ()
102
+
103
+ @staticmethod
104
+ def of(*definitions: _PersistenceDefinition) -> PersistenceSchema:
105
+ attributes: list[Attribute[Any] | AttributeMap[Any]] = []
106
+ channels: list[Channel[Any] | ChannelMap[Any]] = []
107
+ for definition in definitions:
108
+ if isinstance(definition, (Attribute, AttributeMap)):
109
+ attributes.append(definition)
110
+ elif isinstance(definition, (Channel, ChannelMap)):
111
+ channels.append(definition)
112
+ else:
113
+ raise TypeError("unsupported persistence definition")
114
+ return PersistenceSchema(tuple(attributes), tuple(channels))
115
+
116
+
117
+ class Flow(Generic[StartT], ABC):
118
+ def get_flow_type(self) -> str:
119
+ return type(self).__name__
120
+
121
+ def get_steps(self) -> StepList[StartT]:
122
+ return StepList.empty()
123
+
124
+ def get_persistence_schema(self) -> PersistenceSchema:
125
+ return PersistenceSchema.of()
126
+
127
+
128
+ @dataclass(frozen=True)
129
+ class _RegisteredStep:
130
+ name: str
131
+ step: Step[Any]
132
+ input_codec: Codec[Any]
133
+ starting: bool
134
+ skips_wait_for: bool
135
+
136
+
137
+ @dataclass(frozen=True)
138
+ class _RegisteredRPC:
139
+ name: str
140
+ method: Callable[..., Any]
141
+ options: _RPCOptions
142
+ input_codec: Codec[Any] | None
143
+ output_codec: Codec[Any] | None
144
+ locks: tuple[str, ...]
145
+
146
+
147
+ @dataclass(frozen=True)
148
+ class _RegisteredFlow:
149
+ name: str
150
+ flow: Flow[Any]
151
+ steps: MappingProxyType[str, _RegisteredStep]
152
+ start_step: _RegisteredStep | None
153
+ rpcs: MappingProxyType[str, _RegisteredRPC]
154
+ persistence: MappingProxyType[str, _PersistenceDefinition]
155
+
156
+ def step(self, name: str) -> _RegisteredStep:
157
+ try:
158
+ return self.steps[name]
159
+ except KeyError as error:
160
+ raise ValueError(f"Step is not registered: {name}") from error
161
+
162
+ def rpc(self, name: str) -> _RegisteredRPC:
163
+ try:
164
+ return self.rpcs[name]
165
+ except KeyError as error:
166
+ raise ValueError(f"RPC is not registered: {name}") from error
167
+
168
+
169
+ @dataclass(frozen=True)
170
+ class Registry:
171
+ flows: tuple[Flow[Any], ...]
172
+ codec_registry: CodecRegistry
173
+ _steps: tuple[_RegisteredStep, ...]
174
+ _rpcs: tuple[_RegisteredRPC, ...]
175
+ _registered_flows: MappingProxyType[str, _RegisteredFlow]
176
+
177
+ def __init__(
178
+ self,
179
+ flows: Sequence[Flow[Any]],
180
+ codec_registry: CodecRegistry | None = None,
181
+ ) -> None:
182
+ immutable_flows = tuple(flows)
183
+ resolved_codecs = codec_registry or CodecRegistry()
184
+ registered_flows = self._assemble(immutable_flows, resolved_codecs)
185
+ registered_steps = tuple(
186
+ step
187
+ for registered_flow in registered_flows.values()
188
+ for step in registered_flow.steps.values()
189
+ )
190
+ registered_rpcs = tuple(
191
+ registered_rpc
192
+ for registered_flow in registered_flows.values()
193
+ for registered_rpc in registered_flow.rpcs.values()
194
+ )
195
+ object.__setattr__(self, "flows", immutable_flows)
196
+ object.__setattr__(self, "codec_registry", resolved_codecs)
197
+ object.__setattr__(self, "_steps", registered_steps)
198
+ object.__setattr__(self, "_rpcs", registered_rpcs)
199
+ object.__setattr__(
200
+ self,
201
+ "_registered_flows",
202
+ MappingProxyType(registered_flows),
203
+ )
204
+
205
+ @staticmethod
206
+ def _assemble(
207
+ flows: tuple[Flow[Any], ...], codec_registry: CodecRegistry
208
+ ) -> dict[str, _RegisteredFlow]:
209
+ registered_flows: dict[str, _RegisteredFlow] = {}
210
+ for flow in flows:
211
+ if not isinstance(flow, Flow):
212
+ raise TypeError("Flow definition is invalid")
213
+ flow_name = flow.get_flow_type()
214
+ require_name(flow_name)
215
+ if flow_name in registered_flows:
216
+ raise ValueError(f"duplicate Flow {flow_name}")
217
+ registered_flows[flow_name] = Registry._assemble_flow(
218
+ flow_name,
219
+ flow,
220
+ codec_registry,
221
+ )
222
+ return registered_flows
223
+
224
+ @staticmethod
225
+ def _assemble_flow(
226
+ flow_name: str,
227
+ flow: Flow[Any],
228
+ codec_registry: CodecRegistry,
229
+ ) -> _RegisteredFlow:
230
+ definitions = flow.get_steps()
231
+ if not isinstance(definitions, StepList):
232
+ raise TypeError("Flow steps must be a StepList")
233
+ registered_steps: dict[str, _RegisteredStep] = {}
234
+ start_step: _RegisteredStep | None = None
235
+ for definition in definitions:
236
+ if not isinstance(definition, _StepDef):
237
+ raise TypeError("Flow StepList contains an invalid definition")
238
+ if definition.is_start_step:
239
+ if start_step is not None:
240
+ raise ValueError("Flow must not have multiple start Steps")
241
+ step = definition.step
242
+ step_name = step.get_step_type()
243
+ require_name(step_name)
244
+ if step_name in registered_steps:
245
+ raise ValueError(f"duplicate Step {step_name}")
246
+ registered_step = _RegisteredStep(
247
+ step_name,
248
+ step,
249
+ Registry._step_input_codec(step, codec_registry),
250
+ definition.is_start_step,
251
+ type(step).wait_for is Step.wait_for,
252
+ )
253
+ registered_steps[step_name] = registered_step
254
+ if definition.is_start_step:
255
+ start_step = registered_step
256
+
257
+ schema = flow.get_persistence_schema()
258
+ if not isinstance(schema, PersistenceSchema):
259
+ raise TypeError("Flow persistence schema must be a PersistenceSchema")
260
+ persistence = Registry._assemble_persistence(schema)
261
+ registered_rpcs: dict[str, _RegisteredRPC] = {}
262
+ for attribute_name in dir(flow):
263
+ method = getattr(flow, attribute_name)
264
+ function = getattr(method, "__func__", method)
265
+ options = getattr(function, "__dex_rpc_options__", None)
266
+ if not isinstance(options, _RPCOptions):
267
+ continue
268
+ rpc_name = options.name or attribute_name
269
+ require_name(rpc_name)
270
+ if rpc_name in registered_rpcs:
271
+ raise ValueError(f"duplicate RPC {rpc_name}")
272
+ Registry._validate_rpc_locks(rpc_name, options, schema)
273
+ input_codec, output_codec = Registry._rpc_codecs(method, codec_registry)
274
+ registered_rpcs[rpc_name] = _RegisteredRPC(
275
+ rpc_name,
276
+ method,
277
+ options,
278
+ input_codec,
279
+ output_codec,
280
+ tuple(
281
+ Registry._physical_lock(lock) for lock in options.lock_attributes
282
+ ),
283
+ )
284
+ return _RegisteredFlow(
285
+ flow_name,
286
+ flow,
287
+ MappingProxyType(registered_steps),
288
+ start_step,
289
+ MappingProxyType(registered_rpcs),
290
+ MappingProxyType(persistence),
291
+ )
292
+
293
+ @staticmethod
294
+ def _assemble_persistence(
295
+ schema: PersistenceSchema,
296
+ ) -> dict[str, _PersistenceDefinition]:
297
+ persistence: dict[str, _PersistenceDefinition] = {}
298
+ for definition in (*schema.attributes, *schema.channels):
299
+ if definition.name in persistence:
300
+ raise ValueError(f"duplicate persistence definition {definition.name}")
301
+ persistence[definition.name] = definition
302
+ return persistence
303
+
304
+ @staticmethod
305
+ def _validate_rpc_locks(
306
+ rpc_name: str,
307
+ options: _RPCOptions,
308
+ schema: PersistenceSchema,
309
+ ) -> None:
310
+ lock_identities: set[tuple[int, str | None]] = set()
311
+ for lock in options.lock_attributes:
312
+ if not isinstance(lock, AttributeLock):
313
+ raise TypeError(f"RPC {rpc_name} has an invalid attribute lock")
314
+ if all(lock.attribute is not attribute for attribute in schema.attributes):
315
+ raise ValueError(f"RPC {rpc_name} locks an unregistered attribute")
316
+ if isinstance(lock.attribute, AttributeMap):
317
+ if lock.instance is None:
318
+ raise ValueError(
319
+ f"RPC {rpc_name} attribute-map lock needs an instance"
320
+ )
321
+ require_name(lock.instance)
322
+ elif lock.instance is not None:
323
+ raise ValueError(
324
+ f"RPC {rpc_name} attribute lock cannot have an instance"
325
+ )
326
+ identity = (id(lock.attribute), lock.instance)
327
+ if identity in lock_identities:
328
+ raise ValueError(f"RPC {rpc_name} has a duplicate attribute lock")
329
+ lock_identities.add(identity)
330
+
331
+ @staticmethod
332
+ def _step_input_codec(step: Step[Any], codec_registry: CodecRegistry) -> Codec[Any]:
333
+ input_type = Registry._step_handler_input_type(
334
+ step,
335
+ "execute",
336
+ step.execute,
337
+ StepDecision,
338
+ )
339
+ if type(step).wait_for is not Step.wait_for:
340
+ wait_input_type = Registry._step_handler_input_type(
341
+ step,
342
+ "wait_for",
343
+ step.wait_for,
344
+ Wait,
345
+ )
346
+ if wait_input_type != input_type:
347
+ raise TypeError(
348
+ f"Step {step.get_step_type()} handlers must use the same input type"
349
+ )
350
+ return codec_registry.resolve(input_type)
351
+
352
+ @staticmethod
353
+ def _step_handler_input_type(
354
+ step: Step[Any],
355
+ handler_name: str,
356
+ handler: Callable[..., Any],
357
+ return_type: type[Any],
358
+ ) -> Any:
359
+ if iscoroutinefunction(handler):
360
+ raise TypeError(
361
+ f"Step {step.get_step_type()} {handler_name} must be synchronous"
362
+ )
363
+ parameters = tuple(signature(handler).parameters.values())
364
+ hints = get_type_hints(handler)
365
+ if len(parameters) != 2:
366
+ raise TypeError(
367
+ f"Step {step.get_step_type()} {handler_name} must accept context and input"
368
+ )
369
+ context_parameter, input_parameter = parameters
370
+ if hints.get(context_parameter.name) is not Context:
371
+ raise TypeError(
372
+ f"Step {step.get_step_type()} {handler_name} context must be Context"
373
+ )
374
+ input_type = hints.get(input_parameter.name)
375
+ if input_type is None:
376
+ raise TypeError(
377
+ f"Step {step.get_step_type()} {handler_name} input must be annotated"
378
+ )
379
+ if hints.get("return") is not return_type:
380
+ raise TypeError(
381
+ f"Step {step.get_step_type()} {handler_name} must return "
382
+ f"{return_type.__name__}"
383
+ )
384
+ return input_type
385
+
386
+ @staticmethod
387
+ def _rpc_codecs(
388
+ method: Callable[..., Any], codec_registry: CodecRegistry
389
+ ) -> tuple[Codec[Any] | None, Codec[Any] | None]:
390
+ if iscoroutinefunction(method):
391
+ raise TypeError("RPC must be synchronous")
392
+ parameters = tuple(signature(method).parameters.values())
393
+ hints = get_type_hints(method)
394
+ if len(parameters) not in (1, 2) or "return" not in hints:
395
+ raise TypeError("RPC must annotate Context, optional input, and return")
396
+ if hints.get(parameters[0].name) is not Context:
397
+ raise TypeError("RPC context must be Context")
398
+ if len(parameters) == 2 and parameters[1].name not in hints:
399
+ raise TypeError("RPC input must be annotated")
400
+ input_codec = (
401
+ codec_registry.resolve(hints[parameters[1].name])
402
+ if len(parameters) == 2
403
+ else None
404
+ )
405
+ return_type = hints["return"]
406
+ output_codec = None
407
+ if get_origin(return_type) is RPCResult:
408
+ arguments = get_args(return_type)
409
+ if len(arguments) != 1:
410
+ raise TypeError("RPCResult must declare one output type")
411
+ output_codec = codec_registry.resolve(arguments[0])
412
+ elif return_type not in (None, type(None)):
413
+ raise TypeError("RPC must return RPCResult[O] or None")
414
+ return input_codec, output_codec
415
+
416
+ @staticmethod
417
+ def physical_name(name: str, instance: str) -> str:
418
+ require_name(instance)
419
+ return f"{name}/{quote(instance, safe='')}"
420
+
421
+ @staticmethod
422
+ def _physical_lock(lock: AttributeLock) -> str:
423
+ if lock.instance is None:
424
+ return lock.attribute.name
425
+ return Registry.physical_name(lock.attribute.name, lock.instance)
426
+
427
+ def _flow_by_type(self, flow_type: str) -> _RegisteredFlow:
428
+ try:
429
+ return self._registered_flows[flow_type]
430
+ except KeyError as error:
431
+ raise ValueError(f"Flow is not registered: {flow_type}") from error
432
+
433
+ def _flow_for_instance(self, flow: Flow[Any]) -> _RegisteredFlow:
434
+ registered = self._flow_by_type(flow.get_flow_type())
435
+ if registered.flow is not flow:
436
+ raise ValueError("Flow instance is not registered")
437
+ return registered
438
+
439
+ def _rpc_for_method(
440
+ self,
441
+ method: Callable[..., Any],
442
+ ) -> tuple[_RegisteredFlow, _RegisteredRPC]:
443
+ receiver = getattr(method, "__self__", None)
444
+ function = getattr(method, "__func__", method)
445
+ for flow in self._registered_flows.values():
446
+ if receiver is not flow.flow:
447
+ continue
448
+ for registered_rpc in flow.rpcs.values():
449
+ registered_function = getattr(
450
+ registered_rpc.method,
451
+ "__func__",
452
+ registered_rpc.method,
453
+ )
454
+ if registered_function is function:
455
+ return flow, registered_rpc
456
+ raise ValueError("RPC method is not registered")
dex/flow_config.py ADDED
@@ -0,0 +1,29 @@
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 dataclasses import dataclass
10
+ from enum import Enum
11
+
12
+ from dex.step import StepDurability
13
+ from dex.worker_options import WorkerTarget
14
+
15
+
16
+ class ActiveStepSearchMode(Enum):
17
+ DEFAULT = "default"
18
+ ALL = "all"
19
+ WITH_WAIT_FOR = "with_wait_for"
20
+ DISABLED = "disabled"
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class FlowConfig:
25
+ active_step_search_mode: ActiveStepSearchMode | None = None
26
+ continue_as_new_threshold: int | None = None
27
+ continue_as_new_page_size_bytes: int | None = None
28
+ step_durability: StepDurability | None = None
29
+ worker_target: WorkerTarget | None = None
dex/flow_info.py ADDED
@@ -0,0 +1,51 @@
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 dataclasses import dataclass
10
+ from datetime import datetime
11
+ from enum import Enum
12
+ from typing import Mapping
13
+
14
+ from dex.codec import Value
15
+
16
+
17
+ class FlowStatus(Enum):
18
+ RUNNING = "running"
19
+ COMPLETED = "completed"
20
+ FAILED = "failed"
21
+ CANCELED = "canceled"
22
+ TERMINATED = "terminated"
23
+ TIMED_OUT = "timed_out"
24
+ CONTINUED_AS_NEW = "continued_as_new"
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class FlowInfo:
29
+ flow_id: str
30
+ run_id: str
31
+ flow_type: str
32
+ status: FlowStatus
33
+ started_at: datetime
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class HealthInfo:
38
+ condition: str
39
+ hostname: str
40
+ duration_seconds: int
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class SearchFlowEntry:
45
+ flow_id: str
46
+ run_id: str
47
+ flow_type: str
48
+ status: str
49
+ started_at: datetime
50
+ closed_at: datetime | None
51
+ attributes: Mapping[str, Value]