dex-python-sdk 0.0.1__py3-none-any.whl

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