cadence-python-client 0.2.2__py3-none-any.whl → 0.3.0__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 (32) hide show
  1. cadence/_internal/rpc/error.py +24 -24
  2. cadence/_internal/rpc/retry.py +17 -13
  3. cadence/_internal/rpc/yarpc.py +7 -8
  4. cadence/_internal/workflow/context.py +158 -1
  5. cadence/_internal/workflow/memo.py +40 -0
  6. cadence/_internal/workflow/statemachine/child_workflow_execution_state_machine.py +184 -0
  7. cadence/_internal/workflow/statemachine/decision_manager.py +78 -13
  8. cadence/_internal/workflow/statemachine/event_dispatcher.py +30 -5
  9. cadence/_internal/workflow/statemachine/nondeterminism.py +88 -0
  10. cadence/_internal/workflow/statemachine/signal_external_workflow_state_machine.py +74 -0
  11. cadence/_internal/workflow/workflow_engine.py +26 -2
  12. cadence/_internal/workflow/workflow_instance.py +33 -0
  13. cadence/api/v1/common_pb2.py +13 -31
  14. cadence/api/v1/common_pb2.pyi +2 -31
  15. cadence/api/v1/schedule_pb2.py +8 -8
  16. cadence/api/v1/schedule_pb2.pyi +6 -2
  17. cadence/api/v1/service_domain_pb2.py +32 -32
  18. cadence/api/v1/service_domain_pb2.pyi +4 -2
  19. cadence/api/v1/tasklist_pb2.py +30 -30
  20. cadence/api/v1/tasklist_pb2.pyi +4 -2
  21. cadence/client.py +317 -3
  22. cadence/contrib/openai/cadence_tool.py +6 -0
  23. cadence/error.py +39 -0
  24. cadence/query.py +103 -0
  25. cadence/worker/_decision_task_handler.py +114 -15
  26. cadence/workflow.py +227 -8
  27. {cadence_python_client-0.2.2.dist-info → cadence_python_client-0.3.0.dist-info}/METADATA +54 -61
  28. {cadence_python_client-0.2.2.dist-info → cadence_python_client-0.3.0.dist-info}/RECORD +32 -28
  29. {cadence_python_client-0.2.2.dist-info → cadence_python_client-0.3.0.dist-info}/WHEEL +0 -0
  30. {cadence_python_client-0.2.2.dist-info → cadence_python_client-0.3.0.dist-info}/licenses/LICENSE +0 -0
  31. {cadence_python_client-0.2.2.dist-info → cadence_python_client-0.3.0.dist-info}/licenses/NOTICE +0 -0
  32. {cadence_python_client-0.2.2.dist-info → cadence_python_client-0.3.0.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,7 @@
1
- from typing import Callable, Any, Optional, Generator, TypeVar
1
+ from typing import Callable, Any, Optional, Generator, TypeVar, cast
2
2
 
3
3
  import grpc
4
- from google.rpc.status_pb2 import Status # type: ignore
4
+ from google.rpc.status_pb2 import Status
5
5
  from grpc.aio import (
6
6
  UnaryUnaryClientInterceptor,
7
7
  ClientCallDetails,
@@ -9,7 +9,7 @@ from grpc.aio import (
9
9
  UnaryUnaryCall,
10
10
  Metadata,
11
11
  )
12
- from grpc_status.rpc_status import from_call # type: ignore
12
+ from grpc_status.rpc_status import from_call
13
13
 
14
14
  from cadence.api.v1 import error_pb2
15
15
  from cadence import error
@@ -46,22 +46,22 @@ class CadenceErrorUnaryUnaryCall(UnaryUnaryCall[RequestType, ResponseType]):
46
46
  return await self._wrapped.code()
47
47
 
48
48
  async def details(self) -> str:
49
- return await self._wrapped.details() # type: ignore
49
+ return await self._wrapped.details()
50
50
 
51
51
  async def wait_for_connection(self) -> None:
52
52
  await self._wrapped.wait_for_connection()
53
53
 
54
54
  def cancelled(self) -> bool:
55
- return self._wrapped.cancelled() # type: ignore
55
+ return self._wrapped.cancelled()
56
56
 
57
57
  def done(self) -> bool:
58
- return self._wrapped.done() # type: ignore
58
+ return self._wrapped.done()
59
59
 
60
60
  def time_remaining(self) -> Optional[float]:
61
- return self._wrapped.time_remaining() # type: ignore
61
+ return self._wrapped.time_remaining()
62
62
 
63
63
  def cancel(self) -> bool:
64
- return self._wrapped.cancel() # type: ignore
64
+ return self._wrapped.cancel()
65
65
 
66
66
  def add_done_callback(self, callback: DoneCallbackType) -> None:
67
67
  self._wrapped.add_done_callback(callback)
@@ -79,16 +79,18 @@ class CadenceErrorInterceptor(UnaryUnaryClientInterceptor):
79
79
 
80
80
 
81
81
  def map_error(e: AioRpcError) -> error.CadenceRpcError:
82
- status: Status | None = from_call(e)
82
+ # AioRpcError implements the grpc.Call interface but doesn't inherit from it in the type stubs
83
+ status: Status | None = from_call(cast(grpc.Call, e))
84
+ message = e.details() or ""
83
85
  if not status or not status.details:
84
- return error.CadenceRpcError(e.details(), e.code())
86
+ return error.CadenceRpcError(message, e.code())
85
87
 
86
88
  details = status.details[0]
87
89
  if details.Is(error_pb2.WorkflowExecutionAlreadyStartedError.DESCRIPTOR):
88
90
  already_started = error_pb2.WorkflowExecutionAlreadyStartedError()
89
91
  details.Unpack(already_started)
90
92
  return error.WorkflowExecutionAlreadyStartedError(
91
- e.details(),
93
+ message,
92
94
  e.code(),
93
95
  already_started.start_request_id,
94
96
  already_started.run_id,
@@ -97,19 +99,19 @@ def map_error(e: AioRpcError) -> error.CadenceRpcError:
97
99
  not_exists = error_pb2.EntityNotExistsError()
98
100
  details.Unpack(not_exists)
99
101
  return error.EntityNotExistsError(
100
- e.details(),
102
+ message,
101
103
  e.code(),
102
104
  not_exists.current_cluster,
103
105
  not_exists.active_cluster,
104
106
  list(not_exists.active_clusters),
105
107
  )
106
108
  elif details.Is(error_pb2.WorkflowExecutionAlreadyCompletedError.DESCRIPTOR):
107
- return error.WorkflowExecutionAlreadyCompletedError(e.details(), e.code())
109
+ return error.WorkflowExecutionAlreadyCompletedError(message, e.code())
108
110
  elif details.Is(error_pb2.DomainNotActiveError.DESCRIPTOR):
109
111
  not_active = error_pb2.DomainNotActiveError()
110
112
  details.Unpack(not_active)
111
113
  return error.DomainNotActiveError(
112
- e.details(),
114
+ message,
113
115
  e.code(),
114
116
  not_active.domain,
115
117
  not_active.current_cluster,
@@ -120,7 +122,7 @@ def map_error(e: AioRpcError) -> error.CadenceRpcError:
120
122
  not_supported = error_pb2.ClientVersionNotSupportedError()
121
123
  details.Unpack(not_supported)
122
124
  return error.ClientVersionNotSupportedError(
123
- e.details(),
125
+ message,
124
126
  e.code(),
125
127
  not_supported.feature_version,
126
128
  not_supported.client_impl,
@@ -129,20 +131,18 @@ def map_error(e: AioRpcError) -> error.CadenceRpcError:
129
131
  elif details.Is(error_pb2.FeatureNotEnabledError.DESCRIPTOR):
130
132
  not_enabled = error_pb2.FeatureNotEnabledError()
131
133
  details.Unpack(not_enabled)
132
- return error.FeatureNotEnabledError(
133
- e.details(), e.code(), not_enabled.feature_flag
134
- )
134
+ return error.FeatureNotEnabledError(message, e.code(), not_enabled.feature_flag)
135
135
  elif details.Is(error_pb2.CancellationAlreadyRequestedError.DESCRIPTOR):
136
- return error.CancellationAlreadyRequestedError(e.details(), e.code())
136
+ return error.CancellationAlreadyRequestedError(message, e.code())
137
137
  elif details.Is(error_pb2.DomainAlreadyExistsError.DESCRIPTOR):
138
- return error.DomainAlreadyExistsError(e.details(), e.code())
138
+ return error.DomainAlreadyExistsError(message, e.code())
139
139
  elif details.Is(error_pb2.LimitExceededError.DESCRIPTOR):
140
- return error.LimitExceededError(e.details(), e.code())
140
+ return error.LimitExceededError(message, e.code())
141
141
  elif details.Is(error_pb2.QueryFailedError.DESCRIPTOR):
142
- return error.QueryFailedError(e.details(), e.code())
142
+ return error.QueryFailedError(message, e.code())
143
143
  elif details.Is(error_pb2.ServiceBusyError.DESCRIPTOR):
144
144
  service_busy = error_pb2.ServiceBusyError()
145
145
  details.Unpack(service_busy)
146
- return error.ServiceBusyError(e.details(), e.code(), service_busy.reason)
146
+ return error.ServiceBusyError(message, e.code(), service_busy.reason)
147
147
  else:
148
- return error.CadenceRpcError(e.details(), e.code())
148
+ return error.CadenceRpcError(message, e.code())
@@ -1,6 +1,6 @@
1
1
  import asyncio
2
2
  from dataclasses import dataclass
3
- from typing import Callable, Any
3
+ from typing import Callable, Any, cast
4
4
 
5
5
  from grpc import StatusCode
6
6
  from grpc.aio import UnaryUnaryClientInterceptor, ClientCallDetails
@@ -59,19 +59,23 @@ class RetryInterceptor(UnaryUnaryClientInterceptor):
59
59
  request: Any,
60
60
  ) -> Any:
61
61
  loop = asyncio.get_running_loop()
62
- expiration_interval = client_call_details.timeout
63
- if expiration_interval is None:
64
- expiration_interval = float("inf")
62
+ expiration_interval = (
63
+ client_call_details.timeout
64
+ if client_call_details.timeout is not None
65
+ else float("inf")
66
+ )
65
67
  start_time = loop.time()
66
68
  deadline = start_time + expiration_interval
67
69
 
68
70
  attempts = 0
69
71
  while True:
70
72
  remaining = deadline - loop.time()
71
- # Namedtuple methods start with an underscore to avoid conflicts and aren't actually private
72
- # noinspection PyProtectedMember
73
- call_details = client_call_details._replace( # type: ignore[attr-defined]
74
- timeout=remaining
73
+ call_details = ClientCallDetails(
74
+ method=client_call_details.method,
75
+ timeout=remaining,
76
+ metadata=client_call_details.metadata,
77
+ credentials=client_call_details.credentials,
78
+ wait_for_ready=client_call_details.wait_for_ready,
75
79
  )
76
80
  rpc_call = await continuation(call_details, request)
77
81
  try:
@@ -98,11 +102,11 @@ class RetryInterceptor(UnaryUnaryClientInterceptor):
98
102
 
99
103
 
100
104
  def is_retryable(err: CadenceRpcError, call_details: ClientCallDetails) -> bool:
101
- # Handle requests to the passive side, matching the Go and Java Clients
102
- if (
103
- call_details.method == GET_WORKFLOW_HISTORY # type: ignore[comparison-overlap]
104
- and isinstance(err, EntityNotExistsError)
105
- ):
105
+ # Handle requests to the passive side, matching the Go and Java Clients.
106
+ # grpc-stubs types method as str, but grpcio corrected it to bytes in v1.75.0
107
+ # (grpc/grpc#39405). Cast to bytes to match the actual runtime type.
108
+ method = cast(bytes, call_details.method)
109
+ if method == GET_WORKFLOW_HISTORY and isinstance(err, EntityNotExistsError):
106
110
  return (
107
111
  err.active_cluster is not None
108
112
  and err.current_cluster is not None
@@ -1,4 +1,4 @@
1
- from typing import Any, Callable, cast
1
+ from typing import Any, Callable
2
2
 
3
3
  from grpc.aio import Metadata
4
4
  from grpc.aio import UnaryUnaryClientInterceptor, ClientCallDetails
@@ -38,11 +38,10 @@ class YarpcMetadataInterceptor(UnaryUnaryClientInterceptor):
38
38
  else:
39
39
  metadata += self._metadata
40
40
 
41
- # Namedtuple methods start with an underscore to avoid conflicts and aren't actually private
42
- # noinspection PyProtectedMember
43
- return cast(
44
- ClientCallDetails,
45
- client_call_details._replace( # type: ignore[attr-defined]
46
- metadata=metadata, timeout=client_call_details.timeout or 60.0
47
- ),
41
+ return ClientCallDetails(
42
+ method=client_call_details.method,
43
+ timeout=client_call_details.timeout or 60.0,
44
+ metadata=metadata,
45
+ credentials=client_call_details.credentials,
46
+ wait_for_ready=client_call_details.wait_for_ready,
48
47
  )
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from contextlib import contextmanager
2
4
  from asyncio import get_running_loop
3
5
  from datetime import timedelta
@@ -5,17 +7,23 @@ from math import ceil
5
7
  from typing import Iterator, Optional, Any, Unpack, Type, cast, Callable
6
8
 
7
9
  from cadence._internal.workflow.deterministic_event_loop import DeterministicEventLoop
10
+ from cadence._internal.workflow.memo import memo_to_proto
8
11
  from cadence._internal.workflow.retry_policy import retry_policy_to_proto
9
12
  from cadence._internal.workflow.statemachine.decision_manager import DecisionManager
10
- from cadence.api.v1.common_pb2 import ActivityType
13
+ from cadence.api.v1 import workflow_pb2
14
+ from cadence.api.v1.common_pb2 import ActivityType, WorkflowType, WorkflowExecution
11
15
  from cadence.api.v1.decision_pb2 import (
12
16
  ScheduleActivityTaskDecisionAttributes,
17
+ SignalExternalWorkflowExecutionDecisionAttributes,
18
+ StartChildWorkflowExecutionDecisionAttributes,
13
19
  StartTimerDecisionAttributes,
14
20
  )
15
21
  from cadence.api.v1.tasklist_pb2 import TaskList, TaskListKind
16
22
  from cadence.data_converter import DataConverter
17
23
  from cadence.workflow import (
18
24
  ActivityOptions,
25
+ ChildWorkflowFuture,
26
+ ChildWorkflowOptions,
19
27
  ResultType,
20
28
  WorkflowContext,
21
29
  WorkflowInfo,
@@ -105,6 +113,131 @@ class Context(WorkflowContext):
105
113
 
106
114
  return cast(ResultType, result)
107
115
 
116
+ async def execute_child_workflow(
117
+ self,
118
+ workflow_type: str,
119
+ result_type: Type[ResultType],
120
+ *args: Any,
121
+ **kwargs: Unpack[ChildWorkflowOptions],
122
+ ) -> ResultType:
123
+ future = await self.start_child_workflow(
124
+ workflow_type, result_type, *args, **kwargs
125
+ )
126
+ return await future
127
+
128
+ async def start_child_workflow(
129
+ self,
130
+ workflow_type: str,
131
+ result_type: Type[ResultType],
132
+ *args: Any,
133
+ **kwargs: Unpack[ChildWorkflowOptions],
134
+ ) -> ChildWorkflowFuture[ResultType]:
135
+ schedule_attributes = self._build_child_workflow_attrs(
136
+ workflow_type, *args, **kwargs
137
+ )
138
+ execution_future, result_future = (
139
+ self._decision_manager.schedule_child_workflow(
140
+ schedule_attributes,
141
+ parent_workflow_run_id=self._info.workflow_run_id,
142
+ )
143
+ )
144
+ workflow_execution = await execution_future
145
+ return ChildWorkflowFuture(
146
+ workflow_id=workflow_execution.workflow_id,
147
+ run_id=workflow_execution.run_id,
148
+ result_future=result_future,
149
+ result_type=result_type,
150
+ data_converter=self.data_converter(),
151
+ )
152
+
153
+ def _build_child_workflow_attrs(
154
+ self,
155
+ workflow_type: str,
156
+ *args: Any,
157
+ **kwargs: Unpack[ChildWorkflowOptions],
158
+ ) -> StartChildWorkflowExecutionDecisionAttributes:
159
+ execution_timeout = kwargs.get("execution_start_to_close_timeout")
160
+ if execution_timeout is None:
161
+ raise ValueError(
162
+ "execution_start_to_close_timeout is required for child workflow execution"
163
+ )
164
+ if execution_timeout <= timedelta(0):
165
+ raise ValueError("execution_start_to_close_timeout must be greater than 0")
166
+
167
+ task_timeout = kwargs.get("task_start_to_close_timeout", timedelta(seconds=10))
168
+ if task_timeout <= timedelta(0):
169
+ raise ValueError("task_start_to_close_timeout must be greater than 0")
170
+
171
+ domain = kwargs.get("domain") or self._info.workflow_domain
172
+ task_list = kwargs.get("task_list") or self._info.workflow_task_list
173
+
174
+ workflow_id = kwargs.get("workflow_id") or ""
175
+
176
+ parent_close_policy = kwargs.get(
177
+ "parent_close_policy",
178
+ workflow_pb2.PARENT_CLOSE_POLICY_TERMINATE,
179
+ )
180
+ workflow_id_reuse_policy = kwargs.get(
181
+ "workflow_id_reuse_policy",
182
+ workflow_pb2.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY,
183
+ )
184
+ if workflow_id_reuse_policy == workflow_pb2.WORKFLOW_ID_REUSE_POLICY_INVALID:
185
+ raise ValueError(
186
+ "workflow_id_reuse_policy cannot be WORKFLOW_ID_REUSE_POLICY_INVALID"
187
+ )
188
+
189
+ child_input = self.data_converter().to_data(list(args))
190
+ schedule_attributes = StartChildWorkflowExecutionDecisionAttributes(
191
+ domain=domain,
192
+ workflow_id=workflow_id,
193
+ workflow_type=WorkflowType(name=workflow_type),
194
+ task_list=TaskList(kind=TaskListKind.TASK_LIST_KIND_NORMAL, name=task_list),
195
+ input=child_input,
196
+ execution_start_to_close_timeout=_round_to_nearest_second(
197
+ execution_timeout
198
+ ),
199
+ task_start_to_close_timeout=_round_to_nearest_second(task_timeout),
200
+ parent_close_policy=parent_close_policy,
201
+ workflow_id_reuse_policy=workflow_id_reuse_policy,
202
+ retry_policy=retry_policy_to_proto(kwargs.get("retry_policy")),
203
+ )
204
+
205
+ cron_schedule = kwargs.get("cron_schedule")
206
+ if cron_schedule:
207
+ schedule_attributes.cron_schedule = cron_schedule
208
+
209
+ memo_proto = memo_to_proto(self.data_converter(), kwargs.get("memo"))
210
+ if memo_proto is not None:
211
+ schedule_attributes.memo.CopyFrom(memo_proto)
212
+
213
+ return schedule_attributes
214
+
215
+ async def signal_child_workflow(
216
+ self,
217
+ child_workflow_id: str,
218
+ signal_name: str,
219
+ *args: Any,
220
+ ) -> None:
221
+ if not child_workflow_id:
222
+ raise ValueError("child_workflow_id must not be empty")
223
+ await self._signal_workflow(
224
+ child_workflow_id, signal_name, args, child_workflow_only=True
225
+ )
226
+
227
+ async def signal_external_workflow(
228
+ self,
229
+ workflow_id: str,
230
+ signal_name: str,
231
+ *args: Any,
232
+ run_id: str = "",
233
+ domain: str = "",
234
+ ) -> None:
235
+ if not workflow_id:
236
+ raise ValueError("workflow_id must not be empty")
237
+ await self._signal_workflow(
238
+ workflow_id, signal_name, args, run_id=run_id, domain=domain
239
+ )
240
+
108
241
  async def start_timer(self, duration: timedelta):
109
242
  if duration.total_seconds() <= 0: # shortcut
110
243
  return
@@ -143,6 +276,30 @@ class Context(WorkflowContext):
143
276
  finally:
144
277
  WorkflowContext._var.reset(token)
145
278
 
279
+ async def _signal_workflow(
280
+ self,
281
+ workflow_id: str,
282
+ signal_name: str,
283
+ args: tuple,
284
+ *,
285
+ run_id: str = "",
286
+ domain: str = "",
287
+ child_workflow_only: bool = False,
288
+ ) -> None:
289
+ if not signal_name:
290
+ raise ValueError("signal_name must not be empty")
291
+ attrs = SignalExternalWorkflowExecutionDecisionAttributes(
292
+ domain=domain or self._info.workflow_domain,
293
+ workflow_execution=WorkflowExecution(
294
+ workflow_id=workflow_id,
295
+ run_id=run_id,
296
+ ),
297
+ signal_name=signal_name,
298
+ input=self.data_converter().to_data(list(args)),
299
+ child_workflow_only=child_workflow_only,
300
+ )
301
+ await self._decision_manager.signal_external_workflow(attrs)
302
+
146
303
 
147
304
  def _round_to_nearest_second(delta: timedelta) -> timedelta:
148
305
  return timedelta(seconds=ceil(delta.total_seconds()))
@@ -0,0 +1,40 @@
1
+ """Convert user memo maps to/from protobuf :class:`cadence.api.v1.common_pb2.Memo`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Mapping
6
+
7
+ from cadence.api.v1 import common_pb2
8
+ from cadence.data_converter import DataConverter
9
+
10
+
11
+ def memo_to_proto(
12
+ data_converter: DataConverter,
13
+ memo: Mapping[str, Any] | None,
14
+ ) -> common_pb2.Memo | None:
15
+ """Serialize ``memo`` to protobuf, or ``None`` if no memo was provided.
16
+
17
+ Each value is encoded as a single-element list via the data converter,
18
+ matching Go/Java SDK behavior (one :class:`Payload` per key).
19
+ """
20
+ if not memo:
21
+ return None
22
+ out = common_pb2.Memo()
23
+ for key, value in memo.items():
24
+ out.fields[key].CopyFrom(data_converter.to_data([value]))
25
+ return out
26
+
27
+
28
+ def memo_from_proto(
29
+ data_converter: DataConverter,
30
+ memo: common_pb2.Memo,
31
+ ) -> dict[str, Any]:
32
+ """Deserialize a protobuf ``Memo`` back to a plain dict.
33
+
34
+ Each field payload was encoded as a single-element list; we unwrap that
35
+ first element to recover the original value.
36
+ """
37
+ return {
38
+ key: data_converter.from_data(payload, [None])[0]
39
+ for key, payload in memo.fields.items()
40
+ }
@@ -0,0 +1,184 @@
1
+ from __future__ import annotations
2
+
3
+ from cadence._internal.workflow.statemachine.decision_state_machine import (
4
+ BaseDecisionStateMachine,
5
+ DecisionFuture,
6
+ DecisionId,
7
+ DecisionState,
8
+ DecisionType,
9
+ )
10
+ from cadence._internal.workflow.statemachine.event_dispatcher import EventDispatcher
11
+ from cadence._internal.workflow.statemachine.nondeterminism import (
12
+ record_immediate_cancel,
13
+ )
14
+ from cadence.api.v1 import decision, history
15
+ from cadence.api.v1.common_pb2 import Payload, WorkflowExecution
16
+ from cadence.error import (
17
+ ChildWorkflowExecutionCanceled,
18
+ ChildWorkflowExecutionFailed,
19
+ ChildWorkflowExecutionTerminated,
20
+ ChildWorkflowExecutionTimedOut,
21
+ StartChildWorkflowExecutionFailed,
22
+ )
23
+
24
+ child_workflow_events = EventDispatcher("initiated_event_id")
25
+
26
+
27
+ class ChildWorkflowExecutionStateMachine(BaseDecisionStateMachine):
28
+ """State machine for StartChildWorkflowExecution and child close events."""
29
+
30
+ request: decision.StartChildWorkflowExecutionDecisionAttributes
31
+ execution: DecisionFuture[WorkflowExecution]
32
+ result: DecisionFuture[Payload]
33
+ _run_id: str | None
34
+
35
+ def __init__(
36
+ self,
37
+ request: decision.StartChildWorkflowExecutionDecisionAttributes,
38
+ execution: DecisionFuture[WorkflowExecution],
39
+ result: DecisionFuture[Payload],
40
+ ) -> None:
41
+ super().__init__()
42
+ self.request = request
43
+ self.execution = execution
44
+ self.result = result
45
+ self._run_id = None
46
+
47
+ def get_id(self) -> DecisionId:
48
+ return DecisionId(DecisionType.CHILD_WORKFLOW, self.request.workflow_id)
49
+
50
+ def get_decision(self) -> decision.Decision | None:
51
+ match self.state:
52
+ case DecisionState.REQUESTED:
53
+ return decision.Decision(
54
+ start_child_workflow_execution_decision_attributes=self.request
55
+ )
56
+ case DecisionState.CANCELED_AFTER_REQUESTED:
57
+ return record_immediate_cancel(self.request)
58
+ case (
59
+ DecisionState.CANCELED_AFTER_RECORDED
60
+ | DecisionState.CANCELED_AFTER_STARTED
61
+ ):
62
+ return decision.Decision(
63
+ request_cancel_external_workflow_execution_decision_attributes=decision.RequestCancelExternalWorkflowExecutionDecisionAttributes(
64
+ domain=self.request.domain,
65
+ workflow_execution=WorkflowExecution(
66
+ workflow_id=self.request.workflow_id,
67
+ run_id=self._run_id or "",
68
+ ),
69
+ child_workflow_only=True,
70
+ )
71
+ )
72
+ case _:
73
+ return None
74
+
75
+ def request_cancel(self) -> bool:
76
+ match self.state:
77
+ case DecisionState.REQUESTED:
78
+ self._transition(DecisionState.CANCELED_AFTER_REQUESTED)
79
+ self.execution.force_cancel()
80
+ self.result.force_cancel()
81
+ return True
82
+ case DecisionState.RECORDED:
83
+ self._transition(DecisionState.CANCELED_AFTER_RECORDED)
84
+ self.execution.force_cancel()
85
+ return True
86
+ case DecisionState.STARTED:
87
+ self._transition(DecisionState.CANCELED_AFTER_STARTED)
88
+ return True
89
+ case _:
90
+ return False
91
+
92
+ @child_workflow_events.event("workflow_id", event_id_is_alias=True)
93
+ def handle_initiated(
94
+ self, _: history.StartChildWorkflowExecutionInitiatedEventAttributes
95
+ ) -> None:
96
+ self._transition(DecisionState.RECORDED)
97
+
98
+ @child_workflow_events.event()
99
+ def handle_initiation_failed(
100
+ self, event: history.StartChildWorkflowExecutionFailedEventAttributes
101
+ ) -> None:
102
+ self._transition(DecisionState.COMPLETED)
103
+ exc = StartChildWorkflowExecutionFailed(
104
+ f"start child failed: {event.cause}",
105
+ cause=event.cause,
106
+ workflow_id=event.workflow_id,
107
+ )
108
+ self.execution.set_exception(exc)
109
+ self.result.set_exception(exc)
110
+
111
+ @child_workflow_events.event()
112
+ def handle_started(
113
+ self, event: history.ChildWorkflowExecutionStartedEventAttributes
114
+ ) -> None:
115
+ self._run_id = event.workflow_execution.run_id
116
+ if self.state is DecisionState.CANCELED_AFTER_RECORDED:
117
+ self._transition(DecisionState.CANCELED_AFTER_STARTED)
118
+ else:
119
+ self._transition(DecisionState.STARTED)
120
+ if not self.execution.done():
121
+ self.execution.set_result(event.workflow_execution)
122
+
123
+ @child_workflow_events.event()
124
+ def handle_completed(
125
+ self, event: history.ChildWorkflowExecutionCompletedEventAttributes
126
+ ) -> None:
127
+ self._transition(DecisionState.COMPLETED)
128
+ self.result.set_result(event.result)
129
+
130
+ @child_workflow_events.event()
131
+ def handle_failed(
132
+ self, event: history.ChildWorkflowExecutionFailedEventAttributes
133
+ ) -> None:
134
+ self._transition(DecisionState.COMPLETED)
135
+ self.result.set_exception(
136
+ ChildWorkflowExecutionFailed(
137
+ event.failure.reason,
138
+ failure=event.failure,
139
+ )
140
+ )
141
+
142
+ @child_workflow_events.event()
143
+ def handle_canceled(
144
+ self, event: history.ChildWorkflowExecutionCanceledEventAttributes
145
+ ) -> None:
146
+ self._transition(DecisionState.COMPLETED)
147
+ self.result.set_exception(
148
+ ChildWorkflowExecutionCanceled(
149
+ "child workflow canceled", details=event.details
150
+ )
151
+ )
152
+
153
+ @child_workflow_events.event()
154
+ def handle_timed_out(
155
+ self, event: history.ChildWorkflowExecutionTimedOutEventAttributes
156
+ ) -> None:
157
+ self._transition(DecisionState.COMPLETED)
158
+ self.result.set_exception(
159
+ ChildWorkflowExecutionTimedOut(
160
+ f"child workflow timed out: {event.timeout_type}",
161
+ timeout_type=int(event.timeout_type),
162
+ )
163
+ )
164
+
165
+ @child_workflow_events.event()
166
+ def handle_terminated(
167
+ self, event: history.ChildWorkflowExecutionTerminatedEventAttributes
168
+ ) -> None:
169
+ self._transition(DecisionState.COMPLETED)
170
+ self.result.set_exception(ChildWorkflowExecutionTerminated())
171
+
172
+ @child_workflow_events.event(
173
+ "workflow_execution.workflow_id", event_id_is_alias=True
174
+ )
175
+ def handle_cancel_initiated(
176
+ self, _: history.RequestCancelExternalWorkflowExecutionInitiatedEventAttributes
177
+ ) -> None:
178
+ self._transition(DecisionState.CANCELLATION_RECORDED)
179
+
180
+ @child_workflow_events.event()
181
+ def handle_cancel_failed(
182
+ self, _: history.RequestCancelExternalWorkflowExecutionFailedEventAttributes
183
+ ) -> None:
184
+ self._transition(DecisionState.STARTED)