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
@@ -0,0 +1,585 @@
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 http
12
+ from dataclasses import dataclass
13
+ from http import HTTPStatus
14
+ from httpx import Timeout
15
+ from typing import Any, List, Optional, Type, TypeVar
16
+
17
+ from dex.client_options import ClientOptions
18
+ from dex.errors import (
19
+ WorkflowDefinitionError,
20
+ WorkflowRPCAcquiringLockFailure,
21
+ WorkflowRPCExecutionError,
22
+ WorkflowStillRunningError,
23
+ parse_unexpected_error,
24
+ process_http_error,
25
+ process_workflow_abnormal_exit_error,
26
+ InvalidArgumentError,
27
+ )
28
+ from dex.dex_api import Client, errors
29
+ from dex.dex_api.api.default import (
30
+ post_api_v1_workflow_dataobjects_get,
31
+ post_api_v1_workflow_dataobjects_set,
32
+ post_api_v1_workflow_get,
33
+ post_api_v1_workflow_reset,
34
+ post_api_v1_workflow_rpc,
35
+ post_api_v1_workflow_search,
36
+ post_api_v1_workflow_searchattributes_get,
37
+ post_api_v1_workflow_searchattributes_set,
38
+ post_api_v1_workflow_signal,
39
+ post_api_v1_workflow_start,
40
+ post_api_v1_workflow_stop,
41
+ post_api_v1_workflow_timer_skip,
42
+ post_api_v_1_workflow_get_with_wait,
43
+ post_api_v_1_workflow_wait_for_state_completion,
44
+ )
45
+ from dex.dex_api.models import (
46
+ EncodedObject,
47
+ ErrorResponse,
48
+ IDReusePolicy,
49
+ PersistenceLoadingPolicy,
50
+ SearchAttribute,
51
+ SearchAttributeKeyAndType,
52
+ WorkflowConfig,
53
+ WorkflowGetDataObjectsRequest,
54
+ WorkflowGetRequest,
55
+ WorkflowGetResponse,
56
+ WorkflowGetSearchAttributesRequest,
57
+ WorkflowGetSearchAttributesResponse,
58
+ WorkflowResetRequest,
59
+ WorkflowRetryPolicy,
60
+ WorkflowRpcRequest,
61
+ WorkflowRpcResponse,
62
+ WorkflowSearchRequest,
63
+ WorkflowSearchResponse,
64
+ WorkflowSignalRequest,
65
+ WorkflowSkipTimerRequest,
66
+ WorkflowStartOptions,
67
+ WorkflowStartRequest,
68
+ WorkflowStateOptions,
69
+ WorkflowStatus,
70
+ WorkflowStopRequest,
71
+ WorkflowAlreadyStartedOptions,
72
+ KeyValue,
73
+ WorkflowSetSearchAttributesRequest,
74
+ WorkflowWaitForStateCompletionRequest,
75
+ )
76
+ from dex.dex_api.models.workflow_get_data_objects_response import (
77
+ WorkflowGetDataObjectsResponse,
78
+ )
79
+ from dex.dex_api.models.workflow_set_data_objects_request import (
80
+ WorkflowSetDataObjectsRequest,
81
+ )
82
+ from dex.dex_api.models.workflow_start_response import WorkflowStartResponse
83
+ from dex.dex_api.types import Response
84
+ from dex.reset_workflow_type_and_options import ResetWorkflowTypeAndOptions
85
+ from dex.stop_workflow_options import StopWorkflowOptions
86
+ from dex.utils.dex_typing import assert_not_unset
87
+ from dex.utils.persistence_utils import get_search_attribute_value
88
+
89
+ @dataclass
90
+ class UnregisteredWorkflowOptions:
91
+ workflow_id_reuse_policy: Optional[IDReusePolicy] = None
92
+ cron_schedule: Optional[str] = None
93
+ workflow_start_delay_seconds: Optional[int] = None
94
+ workflow_retry_policy: Optional[WorkflowRetryPolicy] = None
95
+ start_state_options: Optional[WorkflowStateOptions] = None
96
+ workflow_config_override: Optional[WorkflowConfig] = None
97
+ workflow_already_started_options: Optional[WorkflowAlreadyStartedOptions] = None
98
+ initial_data_attributes: Optional[dict[str, Any]] = None
99
+ wait_for_completion_state_execution_ids: Optional[list[str]] = None
100
+ wait_for_completion_state_ids: Optional[list[str]] = None
101
+ initial_search_attributes: Optional[list[SearchAttribute]] = None
102
+ using_memo_for_data_attributes: Optional[bool] = None
103
+
104
+ T = TypeVar("T")
105
+
106
+ # from https://stackoverflow.com/questions/45028991/best-way-to-extend-httpstatus-with-custom-value
107
+ # HERE BE DRAGONS!
108
+ # DO NOT do this unless you absolutely have to.
109
+ def add_http_status(name, value, phrase, description=""):
110
+ # call our new member factory, it's essentially the `HTTPStatus.__new__` method
111
+ new_status = HTTPStatus.__new_member__(HTTPStatus, value, phrase, description)
112
+ new_status._name_ = name # store the enum's member internal name
113
+ new_status.__objclass__ = (
114
+ HTTPStatus.__class__
115
+ ) # store the enum's member parent class
116
+ setattr(HTTPStatus, name, new_status) # add it to the global HTTPStatus namespace
117
+ HTTPStatus._member_map_[name] = new_status # add it to the name=>member map
118
+ HTTPStatus._member_names_.append(
119
+ name
120
+ ) # append the names so it appears in __members__
121
+ HTTPStatus._value2member_map_[value] = new_status # add it to the value=>member map
122
+
123
+ add_http_status("DEX_CUSTOM_ERROR_1", 420, "DEX_CUSTOM_ERROR_1")
124
+ add_http_status("DEX_CUSTOM_ERROR_2", 450, "DEX_CUSTOM_ERROR_2")
125
+
126
+ class UnregisteredClient:
127
+ def __init__(self, client_options: ClientOptions):
128
+ self.client_options = client_options
129
+ self.api_client = Client(
130
+ base_url=client_options.server_url,
131
+ timeout=Timeout(client_options.api_timeout),
132
+ raise_on_unexpected_status=True,
133
+ )
134
+
135
+ def start_workflow(
136
+ self,
137
+ workflow_type: str,
138
+ workflow_id: str,
139
+ start_state_id: Optional[str],
140
+ workflow_timeout_seconds: int,
141
+ input: Optional[Any],
142
+ options: Optional[UnregisteredWorkflowOptions],
143
+ ) -> str:
144
+ request = WorkflowStartRequest(
145
+ workflow_id=workflow_id,
146
+ dex_worker_url=self.client_options.worker_url,
147
+ dex_workflow_type=workflow_type,
148
+ workflow_timeout_seconds=workflow_timeout_seconds,
149
+ state_input=self.client_options.object_encoder.encode(input),
150
+ )
151
+ if start_state_id:
152
+ request.start_state_id = start_state_id
153
+
154
+ if options:
155
+ if options.start_state_options:
156
+ request.state_options = options.start_state_options
157
+
158
+ start_options = WorkflowStartOptions()
159
+ if options.workflow_id_reuse_policy:
160
+ start_options.id_reuse_policy = options.workflow_id_reuse_policy
161
+ if options.cron_schedule:
162
+ start_options.cron_schedule = options.cron_schedule
163
+ if options.workflow_start_delay_seconds:
164
+ start_options.workflow_start_delay_seconds = (
165
+ options.workflow_start_delay_seconds
166
+ )
167
+ if options.workflow_retry_policy:
168
+ start_options.retry_policy = options.workflow_retry_policy
169
+
170
+ if options.workflow_config_override:
171
+ start_options.workflow_config_override = (
172
+ options.workflow_config_override
173
+ )
174
+
175
+ if options.workflow_already_started_options:
176
+ start_options.workflow_already_started_options = (
177
+ options.workflow_already_started_options
178
+ )
179
+
180
+ if options.initial_search_attributes:
181
+ for search_attribute in options.initial_search_attributes:
182
+ val = get_search_attribute_value(
183
+ assert_not_unset(search_attribute.value_type), search_attribute
184
+ )
185
+ if val is None:
186
+ raise InvalidArgumentError(
187
+ f"search attribute value is not set correctly for key {search_attribute.key} with value type {search_attribute.value_type}"
188
+ )
189
+ start_options.search_attributes = options.initial_search_attributes
190
+
191
+ if options.initial_data_attributes:
192
+ das = []
193
+ for key, value in options.initial_data_attributes.items():
194
+ das.append(
195
+ KeyValue(key, self.client_options.object_encoder.encode(value))
196
+ )
197
+ start_options.data_attributes = das
198
+
199
+ if options.using_memo_for_data_attributes:
200
+ start_options.use_memo_for_data_attributes = (
201
+ options.using_memo_for_data_attributes
202
+ )
203
+
204
+ request.workflow_start_options = start_options
205
+
206
+ if options.wait_for_completion_state_execution_ids:
207
+ request.wait_for_completion_state_execution_ids = (
208
+ options.wait_for_completion_state_execution_ids
209
+ )
210
+
211
+ if options.wait_for_completion_state_ids:
212
+ request.wait_for_completion_state_ids = (
213
+ options.wait_for_completion_state_ids
214
+ )
215
+
216
+ workflow_start_response = handler_error_and_return(
217
+ post_api_v1_workflow_start.sync_detailed(
218
+ client=self.api_client,
219
+ body=request,
220
+ )
221
+ )
222
+ if workflow_start_response is None or not isinstance(
223
+ workflow_start_response, WorkflowStartResponse
224
+ ):
225
+ raise RuntimeError("Failed to parse WorkflowStartResponse")
226
+
227
+ return workflow_start_response.workflow_run_id
228
+
229
+ def get_simple_workflow_result_with_wait(
230
+ self,
231
+ workflow_id: str,
232
+ workflow_run_id: str,
233
+ type_hint: Optional[Type[T]] = None,
234
+ ) -> Optional[T]:
235
+ """For most cases, a workflow only has one result(one completion state)
236
+ Use this API to retrieve the output of the state"""
237
+ request = WorkflowGetRequest(
238
+ workflow_id=workflow_id,
239
+ workflow_run_id=workflow_run_id,
240
+ needs_results=True,
241
+ )
242
+
243
+ try:
244
+ response = post_api_v_1_workflow_get_with_wait.sync_detailed(
245
+ client=self.api_client,
246
+ body=request,
247
+ )
248
+ except errors.UnexpectedStatus as err:
249
+ err_resp = parse_unexpected_error(err)
250
+ if err.status_code == 420:
251
+ raise WorkflowStillRunningError(err.status_code, err_resp)
252
+ else:
253
+ raise RuntimeError(f"unknown error code {err.status_code}")
254
+
255
+ if response.status_code != http.HTTPStatus.OK:
256
+ assert isinstance(response.parsed, ErrorResponse)
257
+ raise process_http_error(response.status_code, response.parsed)
258
+
259
+ parsed = response.parsed
260
+ assert isinstance(parsed, WorkflowGetResponse)
261
+ if parsed.workflow_status != WorkflowStatus.COMPLETED:
262
+ raise process_workflow_abnormal_exit_error(parsed)
263
+
264
+ if not parsed.results or len(parsed.results) == 0:
265
+ return None
266
+ filtered_results = [
267
+ result for result in parsed.results if result.completed_state_output
268
+ ]
269
+ if len(filtered_results) == 0:
270
+ return None
271
+ elif len(filtered_results) == 1:
272
+ result = filtered_results[0]
273
+ else:
274
+ raise WorkflowDefinitionError(
275
+ "workflow must have one or zero state output for using this API"
276
+ )
277
+
278
+ assert isinstance(result.completed_state_output, EncodedObject)
279
+ return self.client_options.object_encoder.decode(
280
+ result.completed_state_output,
281
+ type_hint,
282
+ )
283
+
284
+ def invoke_rpc(
285
+ self,
286
+ input: Any,
287
+ workflow_id: str,
288
+ workflow_run_id: str,
289
+ rpc_name: str,
290
+ timeout_seconds: int,
291
+ data_attribute_policy: Optional[PersistenceLoadingPolicy],
292
+ all_defined_search_attribute_types: list[SearchAttributeKeyAndType],
293
+ return_type_hint: Optional[Type[T]] = None,
294
+ use_memo_for_data_attributes: bool = False,
295
+ search_attribute_policy: Optional[PersistenceLoadingPolicy] = None,
296
+ ) -> Optional[T]:
297
+ request = WorkflowRpcRequest(
298
+ input_=self.client_options.object_encoder.encode(input),
299
+ workflow_id=workflow_id,
300
+ workflow_run_id=workflow_run_id,
301
+ rpc_name=rpc_name,
302
+ timeout_seconds=timeout_seconds,
303
+ use_memo_for_data_attributes=use_memo_for_data_attributes,
304
+ search_attributes=all_defined_search_attribute_types,
305
+ )
306
+ if data_attribute_policy is not None:
307
+ request.data_attributes_loading_policy = data_attribute_policy
308
+ if search_attribute_policy is not None:
309
+ request.search_attributes_loading_policy = search_attribute_policy
310
+
311
+ try:
312
+ response = post_api_v1_workflow_rpc.sync_detailed(
313
+ client=self.api_client,
314
+ body=request,
315
+ )
316
+ except errors.UnexpectedStatus as err:
317
+ err_resp = parse_unexpected_error(err)
318
+ if err.status_code == 420:
319
+ raise WorkflowRPCExecutionError(err.status_code, err_resp)
320
+ if err.status_code == 450:
321
+ raise WorkflowRPCAcquiringLockFailure(err.status_code, err_resp)
322
+ else:
323
+ raise RuntimeError(f"unknown error code {err.status_code}")
324
+
325
+ if response.status_code != http.HTTPStatus.OK:
326
+ assert isinstance(response.parsed, ErrorResponse)
327
+ raise process_http_error(response.status_code, response.parsed)
328
+ assert isinstance(response.parsed, WorkflowRpcResponse)
329
+ return self.client_options.object_encoder.decode(
330
+ response.parsed.output,
331
+ return_type_hint,
332
+ )
333
+
334
+ def signal_workflow(
335
+ self,
336
+ workflow_id: str,
337
+ workflow_run_id: str,
338
+ signal_channel_name: str,
339
+ signal_value: Optional[Any],
340
+ ):
341
+ request = WorkflowSignalRequest(
342
+ workflow_id=workflow_id,
343
+ workflow_run_id=workflow_run_id,
344
+ signal_channel_name=signal_channel_name,
345
+ signal_value=self.client_options.object_encoder.encode(signal_value),
346
+ )
347
+
348
+ response = post_api_v1_workflow_signal.sync_detailed(
349
+ client=self.api_client,
350
+ body=request,
351
+ )
352
+ handler_error_and_return(response)
353
+
354
+ def reset_workflow(
355
+ self,
356
+ workflow_id: str,
357
+ workflow_run_id: str,
358
+ reset_workflow_type_and_options: ResetWorkflowTypeAndOptions,
359
+ ) -> None:
360
+ request = WorkflowResetRequest(
361
+ workflow_id=workflow_id,
362
+ workflow_run_id=workflow_run_id,
363
+ reset_type=reset_workflow_type_and_options.reset_type,
364
+ reason=reset_workflow_type_and_options.reason,
365
+ )
366
+ if reset_workflow_type_and_options.history_event_id:
367
+ request.history_event_id = reset_workflow_type_and_options.history_event_id
368
+ if reset_workflow_type_and_options.history_event_time:
369
+ request.history_event_time = (
370
+ reset_workflow_type_and_options.history_event_time
371
+ )
372
+ if reset_workflow_type_and_options.state_id:
373
+ request.state_id = reset_workflow_type_and_options.state_id
374
+ if reset_workflow_type_and_options.state_execution_id:
375
+ request.state_execution_id = (
376
+ reset_workflow_type_and_options.state_execution_id
377
+ )
378
+ if reset_workflow_type_and_options.skip_signal_reapply:
379
+ request.skip_signal_reapply = (
380
+ reset_workflow_type_and_options.skip_signal_reapply
381
+ )
382
+
383
+ response = post_api_v1_workflow_reset.sync_detailed(
384
+ client=self.api_client,
385
+ body=request,
386
+ )
387
+ return handler_error_and_return(response)
388
+
389
+ def skip_timer_by_command_id(
390
+ self,
391
+ workflow_id: str,
392
+ workflow_run_id: str,
393
+ workflow_state_id: str,
394
+ timer_command_id: str,
395
+ state_execution_number: int,
396
+ ):
397
+ request = WorkflowSkipTimerRequest(
398
+ workflow_id=workflow_id,
399
+ workflow_run_id=workflow_run_id,
400
+ workflow_state_execution_id=f"{workflow_state_id}-{state_execution_number}",
401
+ timer_command_id=timer_command_id,
402
+ )
403
+ response = post_api_v1_workflow_timer_skip.sync_detailed(
404
+ client=self.api_client,
405
+ body=request,
406
+ )
407
+ handler_error_and_return(response)
408
+
409
+ def skip_timer_at_command_index(
410
+ self,
411
+ workflow_id: str,
412
+ workflow_run_id: str,
413
+ workflow_state_id: str,
414
+ state_execution_number: int,
415
+ timer_command_index: int,
416
+ ):
417
+ request = WorkflowSkipTimerRequest(
418
+ workflow_id=workflow_id,
419
+ workflow_run_id=workflow_run_id,
420
+ workflow_state_execution_id=f"{workflow_state_id}-{state_execution_number}",
421
+ timer_command_index=timer_command_index,
422
+ )
423
+ response = post_api_v1_workflow_timer_skip.sync_detailed(
424
+ client=self.api_client,
425
+ body=request,
426
+ )
427
+ handler_error_and_return(response)
428
+
429
+ def stop_workflow(
430
+ self,
431
+ workflow_id: str,
432
+ workflow_run_id: str,
433
+ options: Optional[StopWorkflowOptions] = None,
434
+ ):
435
+ request = WorkflowStopRequest(
436
+ workflow_id=workflow_id,
437
+ workflow_run_id=workflow_run_id,
438
+ )
439
+ if options:
440
+ request.stop_type = options.workflow_stop_type
441
+ request.reason = options.reason
442
+ response = post_api_v1_workflow_stop.sync_detailed(
443
+ client=self.api_client,
444
+ body=request,
445
+ )
446
+ return handler_error_and_return(response)
447
+
448
+ def get_workflow(
449
+ self,
450
+ workflow_id: str,
451
+ workflow_run_id: str,
452
+ ) -> WorkflowGetResponse:
453
+ request = WorkflowGetRequest(
454
+ workflow_id=workflow_id,
455
+ workflow_run_id=workflow_run_id,
456
+ )
457
+ response = post_api_v1_workflow_get.sync_detailed(
458
+ client=self.api_client,
459
+ body=request,
460
+ )
461
+ return handler_error_and_return(response)
462
+
463
+ def get_workflow_data_attributes(
464
+ self,
465
+ workflow_id: str,
466
+ workflow_run_id: str,
467
+ attribute_keys: Optional[List[str]] = None,
468
+ use_memo_for_data_attributes: Optional[bool] = None,
469
+ ) -> WorkflowGetDataObjectsResponse:
470
+ request = WorkflowGetDataObjectsRequest(
471
+ workflow_id=workflow_id,
472
+ workflow_run_id=workflow_run_id,
473
+ )
474
+ if attribute_keys:
475
+ request.keys = attribute_keys
476
+ if use_memo_for_data_attributes is not None:
477
+ request.use_memo_for_data_attributes = use_memo_for_data_attributes
478
+ response = post_api_v1_workflow_dataobjects_get.sync_detailed(
479
+ client=self.api_client,
480
+ body=request,
481
+ )
482
+ return handler_error_and_return(response)
483
+
484
+ def set_workflow_data_attributes(
485
+ self, workflow_id: str, workflow_run_id: str, attrs: dict[str, Any]
486
+ ):
487
+ objects = [
488
+ KeyValue(key, self.client_options.object_encoder.encode(value))
489
+ for key, value in attrs.items()
490
+ ]
491
+
492
+ request = WorkflowSetDataObjectsRequest(
493
+ workflow_id,
494
+ workflow_run_id,
495
+ objects,
496
+ )
497
+
498
+ response = post_api_v1_workflow_dataobjects_set.sync_detailed(
499
+ client=self.api_client, body=request
500
+ )
501
+ return handler_error_and_return(response)
502
+
503
+ def search_workflow(
504
+ self,
505
+ request: WorkflowSearchRequest,
506
+ ) -> WorkflowSearchResponse:
507
+ response = post_api_v1_workflow_search.sync_detailed(
508
+ client=self.api_client,
509
+ body=request,
510
+ )
511
+ return handler_error_and_return(response)
512
+
513
+ def get_workflow_search_attributes(
514
+ self,
515
+ workflow_id: str,
516
+ workflow_run_id: str,
517
+ attribute_keys: Optional[List[SearchAttributeKeyAndType]] = None,
518
+ ) -> WorkflowGetSearchAttributesResponse:
519
+ request = WorkflowGetSearchAttributesRequest(
520
+ workflow_id=workflow_id,
521
+ workflow_run_id=workflow_run_id,
522
+ )
523
+ if attribute_keys:
524
+ request.keys = attribute_keys
525
+ response = post_api_v1_workflow_searchattributes_get.sync_detailed(
526
+ client=self.api_client,
527
+ body=request,
528
+ )
529
+ return handler_error_and_return(response)
530
+
531
+ def set_workflow_search_attributes(
532
+ self,
533
+ workflow_id: str,
534
+ workflow_run_id: str,
535
+ search_attributes: list[SearchAttribute],
536
+ ):
537
+ request = WorkflowSetSearchAttributesRequest(
538
+ workflow_id=workflow_id,
539
+ workflow_run_id=workflow_run_id,
540
+ search_attributes=search_attributes,
541
+ )
542
+ response = post_api_v1_workflow_searchattributes_set.sync_detailed(
543
+ client=self.api_client,
544
+ body=request,
545
+ )
546
+ return handler_error_and_return(response)
547
+
548
+ def wait_for_state_execution_completion_with_state_execution_id(
549
+ self, workflow_id: str, state_execution_id: str
550
+ ):
551
+ request = WorkflowWaitForStateCompletionRequest(
552
+ workflow_id=workflow_id,
553
+ state_execution_id=state_execution_id,
554
+ wait_time_seconds=self.client_options.long_poll_api_max_wait_time_seconds,
555
+ )
556
+
557
+ response = post_api_v_1_workflow_wait_for_state_completion.sync_detailed(
558
+ client=self.api_client,
559
+ body=request,
560
+ )
561
+
562
+ return handler_error_and_return(response)
563
+
564
+ def wait_for_state_execution_completion_with_wait_for_key(
565
+ self, workflow_id: str, state_id: str, wait_for_key: str
566
+ ):
567
+ request = WorkflowWaitForStateCompletionRequest(
568
+ workflow_id=workflow_id,
569
+ state_id=state_id,
570
+ wait_for_key=wait_for_key,
571
+ wait_time_seconds=self.client_options.long_poll_api_max_wait_time_seconds,
572
+ )
573
+
574
+ response = post_api_v_1_workflow_wait_for_state_completion.sync_detailed(
575
+ client=self.api_client,
576
+ body=request,
577
+ )
578
+
579
+ return handler_error_and_return(response)
580
+
581
+ def handler_error_and_return(response: Response):
582
+ if response.status_code != http.HTTPStatus.OK:
583
+ assert isinstance(response.parsed, ErrorResponse)
584
+ raise process_http_error(response.status_code, response.parsed)
585
+ return response.parsed
dex/utils/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ # Legacy Materials in this file remain under their original licenses.
2
+ # See LEGACY_NOTICES.md.
3
+
@@ -0,0 +1,25 @@
1
+ # Legacy Materials in this file remain under their original licenses.
2
+ # See LEGACY_NOTICES.md.
3
+
4
+ # Modifications Copyright (c) 2026 Super Durable, Inc.
5
+ #
6
+ # Modifications after the Legacy Cutoff are licensed under the
7
+ # Super Durable Source License 1.0.
8
+ # Legacy Materials remain under their original licenses.
9
+ # See LICENSE and LEGACY_NOTICES.md.
10
+
11
+ from typing import Optional, TypeVar, Union
12
+
13
+ from dex.dex_api.types import Unset
14
+
15
+ T = TypeVar("T")
16
+
17
+ def unset_to_none(input: Union[Unset, T]) -> Optional[T]:
18
+ if isinstance(input, Unset):
19
+ return None
20
+ return input
21
+
22
+ def assert_not_unset(input: Union[Unset, T]) -> T:
23
+ if isinstance(input, Unset):
24
+ raise RuntimeError("the value shouldn't be Unset")
25
+ return input
@@ -0,0 +1,32 @@
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 dex.dex_api.models import SearchAttributeValueType, SearchAttribute
12
+ from dex.utils.dex_typing import unset_to_none
13
+
14
+ def get_search_attribute_value(
15
+ sa_type: SearchAttributeValueType, attribute: SearchAttribute
16
+ ):
17
+ if (
18
+ sa_type == SearchAttributeValueType.KEYWORD
19
+ or sa_type == SearchAttributeValueType.DATETIME
20
+ or sa_type == SearchAttributeValueType.TEXT
21
+ ):
22
+ return unset_to_none(attribute.string_value)
23
+ elif sa_type == SearchAttributeValueType.INT:
24
+ return unset_to_none(attribute.integer_value)
25
+ elif sa_type == SearchAttributeValueType.DOUBLE:
26
+ return unset_to_none(attribute.double_value)
27
+ elif sa_type == SearchAttributeValueType.BOOL:
28
+ return unset_to_none(attribute.bool_value)
29
+ elif sa_type == SearchAttributeValueType.KEYWORD_ARRAY:
30
+ return unset_to_none(attribute.string_array_value)
31
+ else:
32
+ raise ValueError(f"not supported search attribute value type, {sa_type}")
dex/wait.py ADDED
@@ -0,0 +1,49 @@
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 dataclasses import dataclass
14
+ from enum import Enum
15
+
16
+ from dex.condition import Condition, ConditionCombination
17
+
18
+
19
+ class WaitKind(Enum):
20
+ SKIP_IMMEDIATELY = "skip_immediately"
21
+ ALL_OF = "all_of"
22
+ ANY_OF = "any_of"
23
+ ANY_COMBINATION_OF = "any_combination_of"
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class Wait:
28
+ kind: WaitKind
29
+ conditions: tuple[Condition, ...] = ()
30
+ combinations: tuple[ConditionCombination, ...] = ()
31
+
32
+ @staticmethod
33
+ def skip_immediately() -> Wait:
34
+ return Wait(WaitKind.SKIP_IMMEDIATELY)
35
+
36
+ @staticmethod
37
+ def all_of(*conditions: Condition) -> Wait:
38
+ return Wait(WaitKind.ALL_OF, conditions)
39
+
40
+ @staticmethod
41
+ def any_of(*conditions: Condition) -> Wait:
42
+ return Wait(WaitKind.ANY_OF, conditions)
43
+
44
+ @staticmethod
45
+ def any_combination_of(*combinations: ConditionCombination) -> Wait:
46
+ return Wait(
47
+ WaitKind.ANY_COMBINATION_OF,
48
+ combinations=combinations,
49
+ )