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/client.py ADDED
@@ -0,0 +1,715 @@
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 datetime import timedelta, timezone
14
+ from types import TracebackType
15
+ from typing import Any, Callable, TypeVar, cast, overload
16
+ from uuid import uuid4
17
+
18
+ import grpc
19
+
20
+ from dex._grpc_errors import translate_rpc_error
21
+ from dex._utils import require_name
22
+ from dex._value_hydrator import ValueHydrator
23
+ from dex._value_mapper import ValueMapper
24
+ from dex._worker_dispatcher import WorkerDispatcher
25
+ from dex.attribute import Attribute, AttributeMap
26
+ from dex.blob_cache import BlobCache
27
+ from dex.channel import Channel, ChannelMap
28
+ from dex.client_options import ClientOptions
29
+ from dex.context import Context
30
+ from dex.dexpb import dex_pb2 as pb
31
+ from dex.dexpb import dex_pb2_grpc
32
+ from dex.flow import Flow, Registry, RPCResult
33
+ from dex.flow_config import ActiveStepSearchMode, FlowConfig
34
+ from dex.flow_info import FlowInfo, FlowStatus
35
+ from dex.flow_options import (
36
+ IdReusePolicy,
37
+ ResetFlowOptions,
38
+ ResetType,
39
+ StartFlowOptions,
40
+ StopFlowOptions,
41
+ StopType,
42
+ )
43
+ from dex.runtime_errors import (
44
+ FlowErrorType,
45
+ FlowUncompletedError,
46
+ LongPollTimeoutError,
47
+ )
48
+ from dex.step import RetryPolicy, StepDurability
49
+ from dex.step_execution import StepExecutionId, TimerId
50
+
51
+ InputT = TypeVar("InputT")
52
+ OutputT = TypeVar("OutputT")
53
+ ValueT = TypeVar("ValueT")
54
+
55
+
56
+ class Client:
57
+ def __init__(
58
+ self,
59
+ registry: Registry,
60
+ blob_cache: BlobCache,
61
+ options: ClientOptions | None = None,
62
+ ) -> None:
63
+ self.registry = registry
64
+ self.blob_cache = blob_cache
65
+ self.options = options or ClientOptions()
66
+ self._channel = grpc.insecure_channel(self.options.server_address)
67
+ self._service = dex_pb2_grpc.FlowServiceStub( # type: ignore[no-untyped-call]
68
+ self._channel
69
+ )
70
+ self._values = ValueMapper(registry.codec_registry)
71
+ self._hydrator = ValueHydrator(self._service, blob_cache)
72
+ self._mappings = WorkerDispatcher(
73
+ registry,
74
+ self._values,
75
+ self._hydrator,
76
+ )
77
+ self._closed = False
78
+
79
+ def __enter__(self) -> Client:
80
+ return self
81
+
82
+ def __exit__(
83
+ self,
84
+ exception_type: type[BaseException] | None,
85
+ exception: BaseException | None,
86
+ traceback: TracebackType | None,
87
+ ) -> None:
88
+ self.close()
89
+
90
+ def start_flow(
91
+ self,
92
+ flow: Flow[InputT],
93
+ flow_id: str,
94
+ input: InputT,
95
+ options: StartFlowOptions = StartFlowOptions(),
96
+ ) -> str:
97
+ registered = self.registry._flow_for_instance(flow)
98
+ request = pb.StartFlowRequest(
99
+ flow_id=require_name(flow_id),
100
+ flow_type=registered.name,
101
+ request_id=options.request_id or str(uuid4()),
102
+ flow_start_options=self._map_start_options(options),
103
+ )
104
+ if registered.start_step is not None:
105
+ start = registered.start_step
106
+ request.start_step_type = start.name
107
+ request.step_input.CopyFrom(self._values.encode(input, start.input_codec))
108
+ step_options = self._mappings.map_step_options(
109
+ registered,
110
+ start.step.get_step_options(),
111
+ )
112
+ if step_options is not None:
113
+ request.step_options.CopyFrom(step_options)
114
+ request.step_options.skip_wait_for = start.skips_wait_for
115
+ elif input is not None:
116
+ raise ValueError("Flow without a start Step requires None input")
117
+ if options.timeout is not None:
118
+ request.flow_timeout_seconds = self._seconds32(options.timeout)
119
+ response = cast(
120
+ pb.StartFlowResponse, self._call(self._service.StartFlow, request)
121
+ )
122
+ return response.run_id
123
+
124
+ @overload
125
+ def invoke_rpc(
126
+ self,
127
+ rpc_method: Callable[[Context, InputT], RPCResult[OutputT]],
128
+ flow_id: str,
129
+ input: InputT,
130
+ *,
131
+ run_id: str = "",
132
+ ) -> OutputT: ...
133
+
134
+ @overload
135
+ def invoke_rpc(
136
+ self,
137
+ rpc_method: Callable[[Context], RPCResult[OutputT]],
138
+ flow_id: str,
139
+ *,
140
+ run_id: str = "",
141
+ ) -> OutputT: ...
142
+
143
+ @overload
144
+ def invoke_rpc(
145
+ self,
146
+ rpc_method: Callable[[Context, InputT], None],
147
+ flow_id: str,
148
+ input: InputT,
149
+ *,
150
+ run_id: str = "",
151
+ ) -> None: ...
152
+
153
+ @overload
154
+ def invoke_rpc(
155
+ self,
156
+ rpc_method: Callable[[Context], None],
157
+ flow_id: str,
158
+ *,
159
+ run_id: str = "",
160
+ ) -> None: ...
161
+
162
+ def invoke_rpc(
163
+ self,
164
+ rpc_method: Callable[..., Any],
165
+ flow_id: str,
166
+ input: object = None,
167
+ *,
168
+ run_id: str = "",
169
+ ) -> Any:
170
+ _, rpc = self.registry._rpc_for_method(rpc_method)
171
+ encoded_input = (
172
+ self._values.encode(input, rpc.input_codec)
173
+ if rpc.input_codec is not None
174
+ else self._values.encode_dynamic(None)
175
+ )
176
+ timeout = (
177
+ self._seconds32(rpc.options.timeout)
178
+ if rpc.options.timeout is not None
179
+ else 0
180
+ )
181
+ response = cast(
182
+ pb.InvokeRPCResponse,
183
+ self._call(
184
+ self._service.InvokeRPC,
185
+ pb.InvokeRPCRequest(
186
+ flow_id=require_name(flow_id),
187
+ run_id=run_id,
188
+ rpc_name=rpc.name,
189
+ input=encoded_input,
190
+ timeout_seconds=timeout,
191
+ lock_attribute_keys=rpc.locks,
192
+ request_id=str(uuid4()),
193
+ ),
194
+ ),
195
+ )
196
+ if rpc.output_codec is None:
197
+ return None
198
+ return self._values.decode(
199
+ self._hydrator.hydrate(response.output),
200
+ rpc.output_codec,
201
+ )
202
+
203
+ @overload
204
+ def get_attribute(
205
+ self,
206
+ flow_id: str,
207
+ attribute: Attribute[ValueT],
208
+ *,
209
+ run_id: str = "",
210
+ ) -> ValueT: ...
211
+
212
+ @overload
213
+ def get_attribute(
214
+ self,
215
+ flow_id: str,
216
+ attribute: AttributeMap[ValueT],
217
+ instance: str,
218
+ *,
219
+ run_id: str = "",
220
+ ) -> ValueT: ...
221
+
222
+ def get_attribute(
223
+ self,
224
+ flow_id: str,
225
+ attribute: Attribute[Any] | AttributeMap[Any],
226
+ instance: str | None = None,
227
+ *,
228
+ run_id: str = "",
229
+ ) -> Any:
230
+ key = self._definition_name(attribute, instance)
231
+ response = cast(
232
+ pb.GetAttributesResponse,
233
+ self._call(
234
+ self._service.GetAttributes,
235
+ pb.GetAttributesRequest(
236
+ flow_id=require_name(flow_id),
237
+ run_id=run_id,
238
+ keys=[key],
239
+ ),
240
+ ),
241
+ )
242
+ if not response.attributes:
243
+ return None
244
+ value = self._hydrator.hydrate(response.attributes[0].value)
245
+ return self._values.decode(value, self._values.codec(attribute.value_type))
246
+
247
+ @overload
248
+ def set_attribute(
249
+ self,
250
+ flow_id: str,
251
+ attribute: Attribute[ValueT],
252
+ value: ValueT,
253
+ /,
254
+ *,
255
+ run_id: str = "",
256
+ ) -> None: ...
257
+
258
+ @overload
259
+ def set_attribute(
260
+ self,
261
+ flow_id: str,
262
+ attribute: AttributeMap[ValueT],
263
+ instance: str,
264
+ value: ValueT,
265
+ /,
266
+ *,
267
+ run_id: str = "",
268
+ ) -> None: ...
269
+
270
+ def set_attribute(
271
+ self,
272
+ flow_id: str,
273
+ attribute: Attribute[Any] | AttributeMap[Any],
274
+ /,
275
+ *args: object,
276
+ run_id: str = "",
277
+ ) -> None:
278
+ instance, value = self._definition_value(attribute, args)
279
+ write = pb.AttributeWrite(
280
+ key=self._definition_name(attribute, instance),
281
+ value=self._values.encode(
282
+ value,
283
+ self._values.codec(attribute.value_type),
284
+ ),
285
+ )
286
+ index = self._values.index_config(
287
+ attribute.index,
288
+ isinstance(attribute, AttributeMap),
289
+ )
290
+ if index is not None:
291
+ write.index_config.CopyFrom(index)
292
+ self._call(
293
+ self._service.SetAttributes,
294
+ pb.SetAttributesRequest(
295
+ flow_id=require_name(flow_id),
296
+ run_id=run_id,
297
+ attributes=[write],
298
+ request_id=str(uuid4()),
299
+ ),
300
+ )
301
+
302
+ @overload
303
+ def publish(
304
+ self,
305
+ flow_id: str,
306
+ channel: Channel[ValueT],
307
+ /,
308
+ *values: ValueT,
309
+ run_id: str = "",
310
+ ) -> None: ...
311
+
312
+ @overload
313
+ def publish(
314
+ self,
315
+ flow_id: str,
316
+ channel: ChannelMap[ValueT],
317
+ instance: str,
318
+ /,
319
+ *values: ValueT,
320
+ run_id: str = "",
321
+ ) -> None: ...
322
+
323
+ def publish(
324
+ self,
325
+ flow_id: str,
326
+ channel: Channel[Any] | ChannelMap[Any],
327
+ /,
328
+ *args: object,
329
+ run_id: str = "",
330
+ ) -> None:
331
+ if isinstance(channel, ChannelMap):
332
+ if len(args) < 2 or not isinstance(args[0], str):
333
+ raise TypeError("ChannelMap publish requires instance and values")
334
+ instance = args[0]
335
+ values = args[1:]
336
+ else:
337
+ instance = None
338
+ values = args
339
+ if not values:
340
+ raise ValueError("publish requires at least one value")
341
+ name = self._definition_name(channel, instance)
342
+ codec = self._values.codec(channel.value_type)
343
+ self._call(
344
+ self._service.PublishToChannel,
345
+ pb.PublishToChannelRequest(
346
+ flow_id=require_name(flow_id),
347
+ run_id=run_id,
348
+ messages=[
349
+ pb.ChannelMessage(
350
+ channel_name=name,
351
+ value=self._values.encode(value, codec),
352
+ )
353
+ for value in values
354
+ ],
355
+ ),
356
+ )
357
+
358
+ @overload
359
+ def wait_for_flow(self, flow_id: str) -> None: ...
360
+
361
+ @overload
362
+ def wait_for_flow(
363
+ self,
364
+ flow_id: str,
365
+ output_type: type[OutputT],
366
+ timeout: timedelta | None = None,
367
+ ) -> OutputT: ...
368
+
369
+ def wait_for_flow(
370
+ self,
371
+ flow_id: str,
372
+ output_type: type[Any] | None = None,
373
+ timeout: timedelta | None = None,
374
+ ) -> Any:
375
+ response = self._wait_for_flow_response(flow_id, timeout)
376
+ if output_type is None:
377
+ return None
378
+ codec = self._values.codec(output_type)
379
+ for result in reversed(response.results):
380
+ if result.HasField("completed_step_output"):
381
+ return self._values.decode(
382
+ self._hydrator.hydrate(result.completed_step_output),
383
+ codec,
384
+ )
385
+ return None
386
+
387
+ def stop_flow(
388
+ self,
389
+ flow_id: str,
390
+ options: StopFlowOptions = StopFlowOptions(),
391
+ ) -> None:
392
+ self._call(
393
+ self._service.StopFlow,
394
+ pb.StopFlowRequest(
395
+ flow_id=require_name(flow_id),
396
+ reason=options.reason or "",
397
+ stop_type={
398
+ StopType.CANCEL: pb.STOP_TYPE_CANCEL,
399
+ StopType.TERMINATE: pb.STOP_TYPE_TERMINATE,
400
+ StopType.FAIL: pb.STOP_TYPE_FAIL,
401
+ }[options.type],
402
+ ),
403
+ )
404
+
405
+ def describe_flow(self, flow_id: str) -> FlowInfo:
406
+ response = cast(
407
+ pb.GetFlowSummaryResponse,
408
+ self._call(
409
+ self._service.GetFlowSummary,
410
+ pb.GetFlowSummaryRequest(flow_id=require_name(flow_id)),
411
+ ),
412
+ )
413
+ return FlowInfo(
414
+ response.flow_execution_id.flow_id,
415
+ response.flow_execution_id.run_id,
416
+ response.flow_type,
417
+ self._map_flow_status(response.flow_status),
418
+ response.start_time.ToDatetime(tzinfo=timezone.utc),
419
+ )
420
+
421
+ def reset_flow(self, flow_id: str, options: ResetFlowOptions) -> str:
422
+ request = pb.ResetFlowRequest(
423
+ flow_id=require_name(flow_id),
424
+ reset_type={
425
+ ResetType.BEGINNING: pb.FLOW_RESET_TYPE_BEGINNING,
426
+ ResetType.HISTORY_EVENT_ID: pb.FLOW_RESET_TYPE_HISTORY_EVENT_ID,
427
+ ResetType.HISTORY_EVENT_TIME: pb.FLOW_RESET_TYPE_HISTORY_EVENT_TIME,
428
+ ResetType.STEP_TYPE: pb.FLOW_RESET_TYPE_STEP_TYPE,
429
+ ResetType.STEP_EXECUTION_ID: pb.FLOW_RESET_TYPE_STEP_EXECUTION_ID,
430
+ }[options.type],
431
+ reason=options.reason or "",
432
+ skip_channel_messages_reapply=options.skip_channel_messages_reapply,
433
+ skip_locking_rpc_reapply=options.skip_locking_rpc_reapply,
434
+ )
435
+ if options.history_event_id is not None:
436
+ request.history_event_id = options.history_event_id
437
+ if options.history_event_time is not None:
438
+ request.history_event_time = options.history_event_time.isoformat()
439
+ if options.step_type is not None:
440
+ request.step_type = options.step_type
441
+ if options.step_execution_id is not None:
442
+ request.step_execution_id = options.step_execution_id
443
+ response = cast(
444
+ pb.ResetFlowResponse,
445
+ self._call(self._service.ResetFlow, request),
446
+ )
447
+ return response.run_id
448
+
449
+ def skip_timer(
450
+ self,
451
+ flow_id: str,
452
+ step_execution_id: StepExecutionId,
453
+ timer_id: TimerId,
454
+ ) -> None:
455
+ request = pb.SkipTimerRequest(
456
+ flow_id=require_name(flow_id),
457
+ step_execution_id=(
458
+ f"{step_execution_id.step_type}-{step_execution_id.number}"
459
+ ),
460
+ )
461
+ if timer_id.condition_id is not None:
462
+ request.timer_condition_id = timer_id.condition_id
463
+ if timer_id.condition_index is not None:
464
+ request.timer_condition_index = timer_id.condition_index
465
+ self._call(self._service.SkipTimer, request)
466
+
467
+ def wait_for_step_completion(
468
+ self,
469
+ flow_id: str,
470
+ step_execution_id: StepExecutionId,
471
+ timeout: timedelta,
472
+ ) -> None:
473
+ self._call(
474
+ self._service.WaitForStepCompletion,
475
+ pb.WaitForStepCompletionRequest(
476
+ flow_id=require_name(flow_id),
477
+ step_type=step_execution_id.step_type,
478
+ step_execution_number=str(step_execution_id.number),
479
+ wait_time_seconds=self._seconds32(timeout),
480
+ request_id=str(uuid4()),
481
+ ),
482
+ )
483
+
484
+ def update_flow_config(self, flow_id: str, config: FlowConfig) -> None:
485
+ self._call(
486
+ self._service.UpdateFlowConfig,
487
+ pb.UpdateFlowConfigRequest(
488
+ flow_id=require_name(flow_id),
489
+ flow_config=self._map_flow_config(config),
490
+ ),
491
+ )
492
+
493
+ def trigger_continue_as_new(self, flow_id: str) -> None:
494
+ self._call(
495
+ self._service.TriggerContinueAsNew,
496
+ pb.TriggerContinueAsNewRequest(flow_id=require_name(flow_id)),
497
+ )
498
+
499
+ def health_check(self) -> bool:
500
+ from google.protobuf import empty_pb2
501
+
502
+ self._call(self._service.HealthCheck, empty_pb2.Empty())
503
+ return True
504
+
505
+ def close(self) -> None:
506
+ if self._closed:
507
+ return
508
+ self._closed = True
509
+ self._channel.close()
510
+
511
+ def _wait_for_flow_response(
512
+ self,
513
+ flow_id: str,
514
+ timeout: timedelta | None,
515
+ ) -> pb.WaitForFlowResponse:
516
+ request = pb.WaitForFlowRequest(
517
+ flow_id=require_name(flow_id),
518
+ needs_results=True,
519
+ )
520
+ if timeout is not None:
521
+ request.wait_time_seconds = self._seconds32(timeout)
522
+ try:
523
+ response = cast(pb.WaitForFlowResponse, self._service.WaitForFlow(request))
524
+ except grpc.RpcError as error:
525
+ if error.code() is grpc.StatusCode.DEADLINE_EXCEEDED:
526
+ raise LongPollTimeoutError(flow_id) from error
527
+ raise translate_rpc_error(error) from error
528
+ if response.flow_status != pb.FLOW_STATUS_COMPLETED:
529
+ info = self.describe_flow(flow_id)
530
+ results = self._hydrator.step_outputs(list(response.results))
531
+ raise FlowUncompletedError(
532
+ info.run_id,
533
+ self._map_flow_status(response.flow_status),
534
+ self._map_flow_error_type(response.error_type),
535
+ response.error_message or None,
536
+ results,
537
+ self._values,
538
+ )
539
+ return response
540
+
541
+ def _map_start_options(self, options: StartFlowOptions) -> pb.FlowStartOptions:
542
+ mapped = pb.FlowStartOptions(
543
+ id_reuse_policy={
544
+ IdReusePolicy.DEFAULT: pb.ID_REUSE_POLICY_UNSPECIFIED,
545
+ IdReusePolicy.ALLOW_IF_PREVIOUS_FAILED: (
546
+ pb.ID_REUSE_POLICY_ALLOW_IF_PREVIOUS_EXISTS_ABNORMALLY
547
+ ),
548
+ IdReusePolicy.ALLOW_IF_NOT_RUNNING: (
549
+ pb.ID_REUSE_POLICY_ALLOW_IF_NO_RUNNING
550
+ ),
551
+ IdReusePolicy.ALLOW_TERMINATE_IF_RUNNING: (
552
+ pb.ID_REUSE_POLICY_ALLOW_TERMINATE_IF_RUNNING
553
+ ),
554
+ IdReusePolicy.DISALLOW: pb.ID_REUSE_POLICY_DISALLOW_REUSE,
555
+ }[options.id_reuse_policy],
556
+ cron_schedule=options.cron_schedule or "",
557
+ flow_already_started_options=pb.FlowAlreadyStartedOptions(
558
+ ignore_already_started_error=options.ignore_already_started
559
+ ),
560
+ )
561
+ if options.start_delay is not None:
562
+ mapped.flow_start_delay_seconds = self._seconds32(options.start_delay)
563
+ if options.retry_policy is not None:
564
+ mapped.retry_policy.CopyFrom(self._map_flow_retry(options.retry_policy))
565
+ for initialization in options._attribute_initializations:
566
+ definition = initialization.definition
567
+ key = self._definition_name(definition, initialization.instance)
568
+ mapped.attributes.append(
569
+ pb.AttributeWrite(
570
+ key=key,
571
+ value=self._values.encode(
572
+ initialization.value,
573
+ self._values.codec(definition.value_type),
574
+ ),
575
+ )
576
+ )
577
+ if (
578
+ options.config_override is not None
579
+ or self.options.worker_target is not None
580
+ ):
581
+ mapped.flow_config_override.CopyFrom(
582
+ self._map_flow_config(options.config_override)
583
+ )
584
+ return mapped
585
+
586
+ def _map_flow_config(self, config: FlowConfig | None) -> pb.FlowConfig:
587
+ mapped = pb.FlowConfig()
588
+ if config is not None:
589
+ if config.active_step_search_mode is not None:
590
+ mapped.active_step_search_mode = {
591
+ ActiveStepSearchMode.DEFAULT: (
592
+ pb.ACTIVE_STEP_SEARCH_MODE_UNSPECIFIED
593
+ ),
594
+ ActiveStepSearchMode.ALL: (
595
+ pb.ACTIVE_STEP_SEARCH_MODE_ENABLED_FOR_ALL
596
+ ),
597
+ ActiveStepSearchMode.WITH_WAIT_FOR: (
598
+ pb.ACTIVE_STEP_SEARCH_MODE_ENABLED_FOR_STEPS_WITH_WAIT_FOR
599
+ ),
600
+ ActiveStepSearchMode.DISABLED: (
601
+ pb.ACTIVE_STEP_SEARCH_MODE_DISABLED
602
+ ),
603
+ }[config.active_step_search_mode]
604
+ if config.continue_as_new_threshold is not None:
605
+ mapped.continue_as_new_threshold = config.continue_as_new_threshold
606
+ if config.continue_as_new_page_size_bytes is not None:
607
+ mapped.continue_as_new_page_size_in_bytes = (
608
+ config.continue_as_new_page_size_bytes
609
+ )
610
+ if config.step_durability is not None:
611
+ mapped.step_durability = {
612
+ StepDurability.DEFAULT: pb.STEP_DURABILITY_UNSPECIFIED,
613
+ StepDurability.SYNC: pb.STEP_DURABILITY_SYNC,
614
+ StepDurability.ASYNC: pb.STEP_DURABILITY_ASYNC,
615
+ }[config.step_durability]
616
+ target = (
617
+ config.worker_target
618
+ if config is not None and config.worker_target is not None
619
+ else self.options.worker_target
620
+ )
621
+ if target is not None:
622
+ mapped.worker_target.CopyFrom(
623
+ pb.WorkerTarget(
624
+ address=target.address,
625
+ is_headless_address=target.headless,
626
+ )
627
+ )
628
+ return mapped
629
+
630
+ @staticmethod
631
+ def _map_flow_retry(retry: RetryPolicy) -> pb.FlowRetryPolicy:
632
+ mapped = pb.FlowRetryPolicy(
633
+ backoff_coefficient=retry.backoff_coefficient,
634
+ maximum_attempts=retry.maximum_attempts,
635
+ )
636
+ if retry.initial_interval is not None:
637
+ mapped.initial_interval_seconds = Client._seconds32(retry.initial_interval)
638
+ if retry.maximum_interval is not None:
639
+ mapped.maximum_interval_seconds = Client._seconds32(retry.maximum_interval)
640
+ return mapped
641
+
642
+ @staticmethod
643
+ def _definition_name(
644
+ definition: Attribute[Any] | AttributeMap[Any] | Channel[Any] | ChannelMap[Any],
645
+ instance: str | None,
646
+ ) -> str:
647
+ if isinstance(definition, (AttributeMap, ChannelMap)):
648
+ if instance is None:
649
+ raise ValueError("dynamic definition requires an instance")
650
+ return Registry.physical_name(definition.name, instance)
651
+ if instance is not None:
652
+ raise ValueError("static definition cannot use an instance")
653
+ return definition.name
654
+
655
+ @staticmethod
656
+ def _definition_value(
657
+ definition: Attribute[Any] | AttributeMap[Any],
658
+ args: tuple[object, ...],
659
+ ) -> tuple[str | None, object]:
660
+ if isinstance(definition, Attribute) and len(args) == 1:
661
+ return None, args[0]
662
+ if (
663
+ isinstance(definition, AttributeMap)
664
+ and len(args) == 2
665
+ and isinstance(args[0], str)
666
+ ):
667
+ return args[0], args[1]
668
+ raise TypeError("set_attribute received invalid arguments")
669
+
670
+ @staticmethod
671
+ def _seconds32(duration: timedelta) -> int:
672
+ seconds = duration.total_seconds()
673
+ if seconds < 0 or not seconds.is_integer() or seconds > 2**31 - 1:
674
+ raise ValueError("duration must be whole seconds within int32")
675
+ return int(seconds)
676
+
677
+ @staticmethod
678
+ def _map_flow_status(status: int) -> FlowStatus:
679
+ statuses: dict[int, FlowStatus] = {
680
+ int(pb.FLOW_STATUS_RUNNING): FlowStatus.RUNNING,
681
+ int(pb.FLOW_STATUS_COMPLETED): FlowStatus.COMPLETED,
682
+ int(pb.FLOW_STATUS_FAILED): FlowStatus.FAILED,
683
+ int(pb.FLOW_STATUS_TIMEOUT): FlowStatus.TIMED_OUT,
684
+ int(pb.FLOW_STATUS_TERMINATED): FlowStatus.TERMINATED,
685
+ int(pb.FLOW_STATUS_CANCELED): FlowStatus.CANCELED,
686
+ int(pb.FLOW_STATUS_CONTINUED_AS_NEW): FlowStatus.CONTINUED_AS_NEW,
687
+ }
688
+ try:
689
+ return statuses[status]
690
+ except KeyError as error:
691
+ raise ValueError(f"unknown Flow status {status}") from error
692
+
693
+ @staticmethod
694
+ def _map_flow_error_type(error_type: int) -> FlowErrorType | None:
695
+ error_types: dict[int, FlowErrorType] = {
696
+ int(pb.FLOW_ERROR_TYPE_STEP_DECISION_FAILING_FLOW): (
697
+ FlowErrorType.STEP_DECISION_FAILED
698
+ ),
699
+ int(
700
+ pb.FLOW_ERROR_TYPE_CLIENT_API_FAILING_FLOW
701
+ ): FlowErrorType.CLIENT_API_FAILED,
702
+ int(pb.FLOW_ERROR_TYPE_WORKER_API_FAIL): FlowErrorType.WORKER_API_FAILED,
703
+ int(pb.FLOW_ERROR_TYPE_INVALID_USER_FLOW_CODE): (
704
+ FlowErrorType.INVALID_USER_FLOW_CODE
705
+ ),
706
+ int(pb.FLOW_ERROR_TYPE_INTERNAL): FlowErrorType.INTERNAL,
707
+ }
708
+ return error_types.get(error_type)
709
+
710
+ @staticmethod
711
+ def _call(method: Callable[[Any], Any], request: Any) -> Any:
712
+ try:
713
+ return method(request)
714
+ except grpc.RpcError as error:
715
+ raise translate_rpc_error(error) from error