cadence-python-client 0.1.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 (95) hide show
  1. cadence/__init__.py +18 -0
  2. cadence/_internal/__init__.py +8 -0
  3. cadence/_internal/activity/__init__.py +5 -0
  4. cadence/_internal/activity/_activity_executor.py +113 -0
  5. cadence/_internal/activity/_context.py +58 -0
  6. cadence/_internal/rpc/__init__.py +0 -0
  7. cadence/_internal/rpc/error.py +148 -0
  8. cadence/_internal/rpc/retry.py +104 -0
  9. cadence/_internal/rpc/yarpc.py +42 -0
  10. cadence/_internal/workflow/__init__.py +0 -0
  11. cadence/_internal/workflow/context.py +121 -0
  12. cadence/_internal/workflow/decision_events_iterator.py +161 -0
  13. cadence/_internal/workflow/decisions_helper.py +312 -0
  14. cadence/_internal/workflow/deterministic_event_loop.py +498 -0
  15. cadence/_internal/workflow/history_event_iterator.py +58 -0
  16. cadence/_internal/workflow/statemachine/__init__.py +0 -0
  17. cadence/_internal/workflow/statemachine/activity_state_machine.py +106 -0
  18. cadence/_internal/workflow/statemachine/decision_manager.py +157 -0
  19. cadence/_internal/workflow/statemachine/decision_state_machine.py +87 -0
  20. cadence/_internal/workflow/statemachine/event_dispatcher.py +76 -0
  21. cadence/_internal/workflow/statemachine/timer_state_machine.py +73 -0
  22. cadence/_internal/workflow/workflow_engine.py +245 -0
  23. cadence/_internal/workflow/workflow_intance.py +44 -0
  24. cadence/activity.py +255 -0
  25. cadence/api/v1/__init__.py +92 -0
  26. cadence/api/v1/common_pb2.py +90 -0
  27. cadence/api/v1/common_pb2.pyi +200 -0
  28. cadence/api/v1/common_pb2_grpc.py +24 -0
  29. cadence/api/v1/decision_pb2.py +67 -0
  30. cadence/api/v1/decision_pb2.pyi +225 -0
  31. cadence/api/v1/decision_pb2_grpc.py +24 -0
  32. cadence/api/v1/domain_pb2.py +68 -0
  33. cadence/api/v1/domain_pb2.pyi +145 -0
  34. cadence/api/v1/domain_pb2_grpc.py +24 -0
  35. cadence/api/v1/error_pb2.py +59 -0
  36. cadence/api/v1/error_pb2.pyi +82 -0
  37. cadence/api/v1/error_pb2_grpc.py +24 -0
  38. cadence/api/v1/history_pb2.py +134 -0
  39. cadence/api/v1/history_pb2.pyi +780 -0
  40. cadence/api/v1/history_pb2_grpc.py +24 -0
  41. cadence/api/v1/query_pb2.py +49 -0
  42. cadence/api/v1/query_pb2.pyi +59 -0
  43. cadence/api/v1/query_pb2_grpc.py +24 -0
  44. cadence/api/v1/service_domain_pb2.py +76 -0
  45. cadence/api/v1/service_domain_pb2.pyi +164 -0
  46. cadence/api/v1/service_domain_pb2_grpc.py +327 -0
  47. cadence/api/v1/service_meta_pb2.py +41 -0
  48. cadence/api/v1/service_meta_pb2.pyi +17 -0
  49. cadence/api/v1/service_meta_pb2_grpc.py +97 -0
  50. cadence/api/v1/service_visibility_pb2.py +71 -0
  51. cadence/api/v1/service_visibility_pb2.pyi +149 -0
  52. cadence/api/v1/service_visibility_pb2_grpc.py +362 -0
  53. cadence/api/v1/service_worker_pb2.py +116 -0
  54. cadence/api/v1/service_worker_pb2.pyi +350 -0
  55. cadence/api/v1/service_worker_pb2_grpc.py +743 -0
  56. cadence/api/v1/service_workflow_pb2.py +126 -0
  57. cadence/api/v1/service_workflow_pb2.pyi +395 -0
  58. cadence/api/v1/service_workflow_pb2_grpc.py +861 -0
  59. cadence/api/v1/tasklist_pb2.py +78 -0
  60. cadence/api/v1/tasklist_pb2.pyi +147 -0
  61. cadence/api/v1/tasklist_pb2_grpc.py +24 -0
  62. cadence/api/v1/visibility_pb2.py +47 -0
  63. cadence/api/v1/visibility_pb2.pyi +53 -0
  64. cadence/api/v1/visibility_pb2_grpc.py +24 -0
  65. cadence/api/v1/workflow_pb2.py +89 -0
  66. cadence/api/v1/workflow_pb2.pyi +365 -0
  67. cadence/api/v1/workflow_pb2_grpc.py +24 -0
  68. cadence/client.py +382 -0
  69. cadence/data_converter.py +78 -0
  70. cadence/error.py +111 -0
  71. cadence/metrics/__init__.py +12 -0
  72. cadence/metrics/constants.py +136 -0
  73. cadence/metrics/metrics.py +56 -0
  74. cadence/metrics/prometheus.py +165 -0
  75. cadence/sample/__init__.py +1 -0
  76. cadence/sample/client_example.py +15 -0
  77. cadence/sample/grpc_usage_example.py +230 -0
  78. cadence/sample/simple_usage_example.py +155 -0
  79. cadence/signal.py +174 -0
  80. cadence/worker/__init__.py +13 -0
  81. cadence/worker/_activity.py +60 -0
  82. cadence/worker/_base_task_handler.py +71 -0
  83. cadence/worker/_decision.py +62 -0
  84. cadence/worker/_decision_task_handler.py +285 -0
  85. cadence/worker/_poller.py +64 -0
  86. cadence/worker/_registry.py +245 -0
  87. cadence/worker/_types.py +26 -0
  88. cadence/worker/_worker.py +56 -0
  89. cadence/workflow.py +271 -0
  90. cadence_python_client-0.1.0.dist-info/METADATA +180 -0
  91. cadence_python_client-0.1.0.dist-info/RECORD +95 -0
  92. cadence_python_client-0.1.0.dist-info/WHEEL +5 -0
  93. cadence_python_client-0.1.0.dist-info/licenses/LICENSE +201 -0
  94. cadence_python_client-0.1.0.dist-info/licenses/NOTICE +19 -0
  95. cadence_python_client-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Decision Events Iterator for Cadence workflow orchestration.
4
+
5
+ This module provides functionality to iterate through workflow history events,
6
+ particularly focusing on decision-related events for replay and execution.
7
+ """
8
+
9
+ from dataclasses import dataclass
10
+ from typing import Iterator, List, Optional
11
+
12
+ from cadence._internal.workflow.history_event_iterator import HistoryEventsIterator
13
+ from cadence.api.v1.history_pb2 import HistoryEvent
14
+
15
+
16
+ @dataclass
17
+ class DecisionEvents:
18
+ """
19
+ Represents events for a single decision iteration.
20
+ """
21
+
22
+ input: List[HistoryEvent]
23
+ output: List[HistoryEvent]
24
+ markers: List[HistoryEvent]
25
+ replay: bool
26
+ replay_current_time_milliseconds: int
27
+ next_decision_event_id: int
28
+
29
+ def get_output_event_by_id(self, event_id: int) -> Optional[HistoryEvent]:
30
+ for event in self.input:
31
+ if hasattr(event, "event_id") and event.event_id == event_id:
32
+ return event
33
+ return None
34
+
35
+
36
+ class DecisionEventsIterator(Iterator[DecisionEvents]):
37
+ """
38
+ Iterator for processing decision events from workflow history.
39
+
40
+ This is the main class that processes workflow history events and groups them
41
+ into decision iterations for proper workflow replay and execution.
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ events: List[HistoryEvent],
47
+ ):
48
+ self._events: HistoryEventsIterator = HistoryEventsIterator(events)
49
+ self._next_decision_event_id: Optional[int] = None
50
+ self._replay_current_time_milliseconds: Optional[int] = None
51
+
52
+ def __iter__(self):
53
+ return self
54
+
55
+ def __next__(self) -> DecisionEvents:
56
+ """
57
+ Process the next decision batch.
58
+ 1. Find the next valid decision task started event during replay or last scheduled decision task events for non-replay
59
+ 2. Collect the decision input events before the decision task
60
+ 3. Collect the decision output events after the decision task
61
+
62
+ Relay mode is determined by checking if the decision task is completed or not
63
+ """
64
+ decision_input_events: List[HistoryEvent] = []
65
+ decision_output_events: List[HistoryEvent] = []
66
+ decision_event: Optional[HistoryEvent] = None
67
+ for event in self._events:
68
+ match event.WhichOneof("attributes"):
69
+ case "decision_task_started_event_attributes":
70
+ next_event = self._events.peek()
71
+
72
+ # latest event, not replay, assign started event as decision event insteaad
73
+ if next_event is None:
74
+ decision_event = event
75
+ break
76
+
77
+ match next_event.WhichOneof("attributes"):
78
+ case (
79
+ "decision_task_failed_event_attributes"
80
+ | "decision_task_timed_out_event_attributes"
81
+ ):
82
+ # skip failed / timed out decision tasks and continue searching
83
+ next(self._events)
84
+ continue
85
+ case "decision_task_completed_event_attributes":
86
+ # found decision task completed event, stop
87
+ decision_event = next(self._events)
88
+ break
89
+ case _:
90
+ raise ValueError(
91
+ f"unexpected event type after decision task started event: {next_event}"
92
+ )
93
+
94
+ case _:
95
+ decision_input_events.append(event)
96
+
97
+ if not decision_event:
98
+ raise StopIteration("no decision event found")
99
+
100
+ # collect decision output events
101
+ while self._events.has_next():
102
+ nxt = self._events.peek() if self._events.has_next() else None
103
+ if nxt and not is_decision_event(nxt):
104
+ break
105
+ decision_output_events.append(next(self._events))
106
+
107
+ replay_current_time_milliseconds = decision_event.event_time.ToMilliseconds()
108
+
109
+ replay: bool
110
+ next_decision_event_id: int
111
+ if (
112
+ decision_event.WhichOneof("attributes")
113
+ == "decision_task_completed_event_attributes"
114
+ ): # completed decision task
115
+ replay = True
116
+ next_decision_event_id = decision_event.event_id + 1
117
+ else:
118
+ replay = False
119
+ next_decision_event_id = decision_event.event_id + 2
120
+
121
+ # collect marker events
122
+ markers = [m for m in decision_output_events if is_marker_event(m)]
123
+
124
+ return DecisionEvents(
125
+ input=decision_input_events,
126
+ output=decision_output_events,
127
+ markers=markers,
128
+ replay=replay,
129
+ replay_current_time_milliseconds=replay_current_time_milliseconds,
130
+ next_decision_event_id=next_decision_event_id,
131
+ )
132
+
133
+
134
+ def is_decision_event(event: HistoryEvent) -> bool:
135
+ """Check if an event is a decision output event."""
136
+ return event is not None and event.WhichOneof("attributes") in set(
137
+ [
138
+ "activity_task_scheduled_event_attributes",
139
+ "start_child_workflow_execution_initiated_event_attributes",
140
+ "timer_started_event_attributes",
141
+ "workflow_execution_completed_event_attributes",
142
+ "workflow_execution_failed_event_attributes",
143
+ "workflow_execution_canceled_event_attributes",
144
+ "workflow_execution_continued_as_new_event_attributes",
145
+ "activity_task_cancel_requested_event_attributes",
146
+ "request_cancel_activity_task_failed_event_attributes",
147
+ "timer_canceled_event_attributes",
148
+ "cancel_timer_failed_event_attributes",
149
+ "request_cancel_external_workflow_execution_initiated_event_attributes",
150
+ "marker_recorded_event_attributes",
151
+ "signal_external_workflow_execution_initiated_event_attributes",
152
+ "upsert_workflow_search_attributes_event_attributes",
153
+ ]
154
+ )
155
+
156
+
157
+ def is_marker_event(event: HistoryEvent) -> bool:
158
+ return bool(
159
+ event is not None
160
+ and event.WhichOneof("attributes") == "marker_recorded_event_attributes"
161
+ )
@@ -0,0 +1,312 @@
1
+ """
2
+ DecisionsHelper manages the next decision ID which is used for tracking decision state machines.
3
+
4
+ This helper ensures that decision IDs are properly assigned and tracked to maintain
5
+ consistency in the workflow execution state.
6
+ """
7
+
8
+ import logging
9
+ from dataclasses import dataclass
10
+ from typing import Dict, Optional
11
+
12
+ from cadence._internal.workflow.statemachine.decision_state_machine import (
13
+ DecisionId,
14
+ DecisionType,
15
+ )
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ @dataclass
21
+ class DecisionTracker:
22
+ """Tracks a decision with its ID and current state."""
23
+
24
+ decision_id: DecisionId
25
+ scheduled_event_id: Optional[int] = None
26
+ initiated_event_id: Optional[int] = None
27
+ started_event_id: Optional[int] = None
28
+ is_completed: bool = False
29
+
30
+
31
+ class DecisionsHelper:
32
+ """
33
+ Helper class to manage decision IDs and work with DecisionManager state machines.
34
+
35
+ This class generates unique decision IDs and integrates with the DecisionManager
36
+ state machines for proper decision lifecycle tracking.
37
+ """
38
+
39
+ def __init__(self):
40
+ """
41
+ Initialize the DecisionsHelper with a DecisionManager reference.
42
+ """
43
+ self._next_decision_counters: Dict[DecisionType, int] = {}
44
+ self._tracked_decisions: Dict[str, DecisionTracker] = {}
45
+ self._decision_id_to_key: Dict[str, str] = {}
46
+ logger.debug("DecisionsHelper initialized with DecisionManager integration")
47
+
48
+ def _get_next_counter(self, decision_type: DecisionType) -> int:
49
+ """
50
+ Get the next counter value for a given decision type.
51
+
52
+ Args:
53
+ decision_type: The type of decision
54
+
55
+ Returns:
56
+ The next counter value
57
+ """
58
+ if decision_type not in self._next_decision_counters:
59
+ self._next_decision_counters[decision_type] = 1
60
+ else:
61
+ self._next_decision_counters[decision_type] += 1
62
+
63
+ return self._next_decision_counters[decision_type]
64
+
65
+ def generate_activity_id(self, activity_name: str) -> str:
66
+ """
67
+ Generate a unique activity ID.
68
+
69
+ Args:
70
+ activity_name: The name of the activity
71
+
72
+ Returns:
73
+ A unique activity ID
74
+ """
75
+ counter = self._get_next_counter(DecisionType.ACTIVITY)
76
+ activity_id = f"{activity_name}_{counter}"
77
+
78
+ # Track this decision
79
+ decision_id = DecisionId(DecisionType.ACTIVITY, activity_id)
80
+ tracker = DecisionTracker(decision_id)
81
+ self._tracked_decisions[activity_id] = tracker
82
+ self._decision_id_to_key[str(decision_id)] = activity_id
83
+
84
+ logger.debug(f"Generated activity ID: {activity_id}")
85
+ return activity_id
86
+
87
+ def generate_timer_id(self, timer_name: str = "timer") -> str:
88
+ """
89
+ Generate a unique timer ID.
90
+
91
+ Args:
92
+ timer_name: The name/prefix for the timer
93
+
94
+ Returns:
95
+ A unique timer ID
96
+ """
97
+ counter = self._get_next_counter(DecisionType.TIMER)
98
+ timer_id = f"{timer_name}_{counter}"
99
+
100
+ # Track this decision
101
+ decision_id = DecisionId(DecisionType.TIMER, timer_id)
102
+ tracker = DecisionTracker(decision_id)
103
+ self._tracked_decisions[timer_id] = tracker
104
+ self._decision_id_to_key[str(decision_id)] = timer_id
105
+
106
+ logger.debug(f"Generated timer ID: {timer_id}")
107
+ return timer_id
108
+
109
+ def generate_child_workflow_id(self, workflow_name: str) -> str:
110
+ """
111
+ Generate a unique child workflow ID.
112
+
113
+ Args:
114
+ workflow_name: The name of the child workflow
115
+
116
+ Returns:
117
+ A unique child workflow ID
118
+ """
119
+ counter = self._get_next_counter(DecisionType.CHILD_WORKFLOW)
120
+ workflow_id = f"{workflow_name}_{counter}"
121
+
122
+ # Track this decision
123
+ decision_id = DecisionId(DecisionType.CHILD_WORKFLOW, workflow_id)
124
+ tracker = DecisionTracker(decision_id)
125
+ self._tracked_decisions[workflow_id] = tracker
126
+ self._decision_id_to_key[str(decision_id)] = workflow_id
127
+
128
+ logger.debug(f"Generated child workflow ID: {workflow_id}")
129
+ return workflow_id
130
+
131
+ def generate_marker_id(self, marker_name: str) -> str:
132
+ """
133
+ Generate a unique marker ID.
134
+
135
+ Args:
136
+ marker_name: The name of the marker
137
+
138
+ Returns:
139
+ A unique marker ID
140
+ """
141
+ counter = self._get_next_counter(DecisionType.MARKER)
142
+ marker_id = f"{marker_name}_{counter}"
143
+
144
+ # Track this decision
145
+ decision_id = DecisionId(DecisionType.MARKER, marker_id)
146
+ tracker = DecisionTracker(decision_id)
147
+ self._tracked_decisions[marker_id] = tracker
148
+ self._decision_id_to_key[str(decision_id)] = marker_id
149
+
150
+ logger.debug(f"Generated marker ID: {marker_id}")
151
+ return marker_id
152
+
153
+ def get_decision_tracker(self, decision_key: str) -> Optional[DecisionTracker]:
154
+ """
155
+ Get the decision tracker for a given decision key.
156
+
157
+ Args:
158
+ decision_key: The decision key (activity_id, timer_id, etc.)
159
+
160
+ Returns:
161
+ The DecisionTracker if found, None otherwise
162
+ """
163
+ return self._tracked_decisions.get(decision_key)
164
+
165
+ def update_decision_scheduled(
166
+ self, decision_key: str, scheduled_event_id: int
167
+ ) -> None:
168
+ """
169
+ Update a decision tracker when it gets scheduled.
170
+
171
+ Args:
172
+ decision_key: The decision key
173
+ scheduled_event_id: The event ID when the decision was scheduled
174
+ """
175
+ tracker = self._tracked_decisions.get(decision_key)
176
+ if tracker:
177
+ tracker.scheduled_event_id = scheduled_event_id
178
+ logger.debug(
179
+ f"Updated decision {decision_key} with scheduled event ID {scheduled_event_id}"
180
+ )
181
+ else:
182
+ logger.warning(f"No tracker found for decision key: {decision_key}")
183
+
184
+ def update_decision_initiated(
185
+ self, decision_key: str, initiated_event_id: int
186
+ ) -> None:
187
+ """
188
+ Update a decision tracker when it gets initiated.
189
+
190
+ Args:
191
+ decision_key: The decision key
192
+ initiated_event_id: The event ID when the decision was initiated
193
+ """
194
+ tracker = self._tracked_decisions.get(decision_key)
195
+ if tracker:
196
+ tracker.initiated_event_id = initiated_event_id
197
+ logger.debug(
198
+ f"Updated decision {decision_key} with initiated event ID {initiated_event_id}"
199
+ )
200
+ else:
201
+ logger.warning(f"No tracker found for decision key: {decision_key}")
202
+
203
+ def update_decision_started(self, decision_key: str, started_event_id: int) -> None:
204
+ """
205
+ Update a decision tracker when it gets started.
206
+
207
+ Args:
208
+ decision_key: The decision key
209
+ started_event_id: The event ID when the decision was started
210
+ """
211
+ tracker = self._tracked_decisions.get(decision_key)
212
+ if tracker:
213
+ tracker.started_event_id = started_event_id
214
+ logger.debug(
215
+ f"Updated decision {decision_key} with started event ID {started_event_id}"
216
+ )
217
+ else:
218
+ logger.warning(f"No tracker found for decision key: {decision_key}")
219
+
220
+ def update_decision_completed(self, decision_key: str) -> None:
221
+ """
222
+ Mark a decision as completed.
223
+
224
+ Args:
225
+ decision_key: The decision key
226
+ """
227
+ tracker = self._tracked_decisions.get(decision_key)
228
+ if tracker:
229
+ tracker.is_completed = True
230
+ logger.debug(f"Marked decision {decision_key} as completed")
231
+ else:
232
+ logger.warning(f"No tracker found for decision key: {decision_key}")
233
+
234
+ def _find_decision_by_scheduled_event_id(
235
+ self, scheduled_event_id: int
236
+ ) -> Optional[str]:
237
+ """Find a decision key by its scheduled event ID."""
238
+ for key, tracker in self._tracked_decisions.items():
239
+ if tracker.scheduled_event_id == scheduled_event_id:
240
+ return key
241
+ return None
242
+
243
+ def _find_decision_by_initiated_event_id(
244
+ self, initiated_event_id: int
245
+ ) -> Optional[str]:
246
+ """Find a decision key by its initiated event ID."""
247
+ for key, tracker in self._tracked_decisions.items():
248
+ if tracker.initiated_event_id == initiated_event_id:
249
+ return key
250
+ return None
251
+
252
+ def _find_decision_by_started_event_id(
253
+ self, started_event_id: int
254
+ ) -> Optional[str]:
255
+ """Find a decision key by its started event ID."""
256
+ for key, tracker in self._tracked_decisions.items():
257
+ if tracker.started_event_id == started_event_id:
258
+ return key
259
+ return None
260
+
261
+ def get_pending_decisions_count(self) -> int:
262
+ """
263
+ Get the count of decisions that are not yet completed.
264
+
265
+ Returns:
266
+ The number of pending decisions
267
+ """
268
+ return sum(
269
+ 1
270
+ for tracker in self._tracked_decisions.values()
271
+ if not tracker.is_completed
272
+ )
273
+
274
+ def get_completed_decisions_count(self) -> int:
275
+ """
276
+ Get the count of decisions that have been completed.
277
+
278
+ Returns:
279
+ The number of completed decisions
280
+ """
281
+ return sum(
282
+ 1 for tracker in self._tracked_decisions.values() if tracker.is_completed
283
+ )
284
+
285
+ def reset(self) -> None:
286
+ """Reset all decision tracking state."""
287
+ self._next_decision_counters.clear()
288
+ self._tracked_decisions.clear()
289
+ self._decision_id_to_key.clear()
290
+ logger.debug("DecisionsHelper reset")
291
+
292
+ def get_stats(self) -> Dict[str, int]:
293
+ """
294
+ Get statistics about tracked decisions.
295
+
296
+ Returns:
297
+ Dictionary with decision statistics
298
+ """
299
+ stats = {
300
+ "total_decisions": len(self._tracked_decisions),
301
+ "pending_decisions": self.get_pending_decisions_count(),
302
+ "completed_decisions": self.get_completed_decisions_count(),
303
+ }
304
+
305
+ # Add per-type counts
306
+ for decision_type in DecisionType:
307
+ type_name = decision_type.name.lower()
308
+ stats[f"{type_name}_count"] = self._next_decision_counters.get(
309
+ decision_type, 0
310
+ )
311
+
312
+ return stats