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,285 @@
1
+ import asyncio
2
+ from concurrent.futures import ThreadPoolExecutor
3
+ import logging
4
+ from typing import Optional
5
+
6
+ from cadence._internal.workflow.history_event_iterator import iterate_history_events
7
+ from cadence.api.v1.common_pb2 import Payload
8
+ from cadence.api.v1.service_worker_pb2 import (
9
+ PollForDecisionTaskResponse,
10
+ RespondDecisionTaskCompletedRequest,
11
+ RespondDecisionTaskFailedRequest,
12
+ )
13
+ from cadence.api.v1.workflow_pb2 import DecisionTaskFailedCause
14
+ from cadence.client import Client
15
+ from cadence.worker._base_task_handler import BaseTaskHandler
16
+ from cadence._internal.workflow.workflow_engine import WorkflowEngine, DecisionResult
17
+ from cadence.workflow import WorkflowInfo
18
+ from cadence.worker._registry import Registry
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class DecisionTaskHandler(BaseTaskHandler[PollForDecisionTaskResponse]):
24
+ """
25
+ Task handler for processing decision tasks.
26
+
27
+ This handler processes decision tasks and generates decisions using workflow engines.
28
+ Uses a thread-safe cache to hold workflow engines for concurrent decision task handling.
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ client: Client,
34
+ task_list: str,
35
+ registry: Registry,
36
+ identity: str = "unknown",
37
+ executor: Optional[ThreadPoolExecutor] = None,
38
+ **options,
39
+ ):
40
+ """
41
+ Initialize the decision task handler.
42
+
43
+ Args:
44
+ client: The Cadence client instance
45
+ task_list: The task list name
46
+ registry: Registry containing workflow functions
47
+ identity: The worker identity
48
+ **options: Additional options for the handler
49
+ """
50
+ super().__init__(client, task_list, identity, **options)
51
+ self._registry = registry
52
+ self._executor = executor
53
+
54
+ async def _handle_task_implementation(
55
+ self, task: PollForDecisionTaskResponse
56
+ ) -> None:
57
+ """
58
+ Handle a decision task implementation.
59
+
60
+ Args:
61
+ task: The decision task to handle
62
+ """
63
+ # Extract workflow execution info
64
+ workflow_execution = task.workflow_execution
65
+ workflow_type = task.workflow_type
66
+
67
+ if not workflow_execution or not workflow_type:
68
+ logger.error(
69
+ "Decision task missing workflow execution or type. Task: %r", task
70
+ )
71
+ raise ValueError("Missing workflow execution or type")
72
+
73
+ workflow_id = workflow_execution.workflow_id
74
+ run_id = workflow_execution.run_id
75
+ workflow_type_name = workflow_type.name
76
+
77
+ # This log matches the WorkflowEngine but at task handler level (like Java ReplayDecisionTaskHandler)
78
+ logger.info(
79
+ "Received decision task for workflow",
80
+ extra={
81
+ "workflow_type": workflow_type_name,
82
+ "workflow_id": workflow_id,
83
+ "run_id": run_id,
84
+ "started_event_id": task.started_event_id,
85
+ "attempt": task.attempt,
86
+ "task_token": task.task_token[:16].hex()
87
+ if task.task_token
88
+ else None, # Log partial token for debugging
89
+ },
90
+ )
91
+
92
+ try:
93
+ workflow_definition = self._registry.get_workflow(workflow_type_name)
94
+ except KeyError:
95
+ logger.error(
96
+ "Workflow type not found in registry",
97
+ extra={
98
+ "workflow_type": workflow_type_name,
99
+ "workflow_id": workflow_id,
100
+ "run_id": run_id,
101
+ "error_type": "workflow_not_registered",
102
+ },
103
+ )
104
+ raise KeyError(f"Workflow type '{workflow_type_name}' not found")
105
+
106
+ # fetch full workflow history
107
+ # TODO sticky cache
108
+ workflow_events = [
109
+ event async for event in iterate_history_events(task, self._client)
110
+ ]
111
+
112
+ # Create workflow info and get or create workflow engine from cache
113
+ workflow_info = WorkflowInfo(
114
+ workflow_type=workflow_type_name,
115
+ workflow_domain=self._client.domain,
116
+ workflow_id=workflow_id,
117
+ workflow_run_id=run_id,
118
+ workflow_task_list=self.task_list,
119
+ data_converter=self._client.data_converter,
120
+ )
121
+
122
+ workflow_engine = WorkflowEngine(
123
+ info=workflow_info,
124
+ workflow_definition=workflow_definition,
125
+ )
126
+
127
+ decision_result = await asyncio.get_running_loop().run_in_executor(
128
+ self._executor, workflow_engine.process_decision, workflow_events
129
+ )
130
+
131
+ # Respond with the decisions
132
+ await self._respond_decision_task_completed(task, decision_result)
133
+
134
+ logger.info(
135
+ "Successfully processed decision task",
136
+ extra={
137
+ "workflow_type": workflow_type_name,
138
+ "workflow_id": workflow_id,
139
+ "run_id": run_id,
140
+ "started_event_id": task.started_event_id,
141
+ },
142
+ )
143
+
144
+ async def handle_task_failure(
145
+ self, task: PollForDecisionTaskResponse, error: Exception
146
+ ) -> None:
147
+ """
148
+ Handle decision task processing failure.
149
+
150
+ Args:
151
+ task: The task that failed
152
+ error: The exception that occurred
153
+ """
154
+ # Extract workflow context for error logging (matches Java ReplayDecisionTaskHandler error patterns)
155
+ workflow_execution = task.workflow_execution
156
+ workflow_id = (
157
+ workflow_execution.workflow_id if workflow_execution else "unknown"
158
+ )
159
+ run_id = workflow_execution.run_id if workflow_execution else "unknown"
160
+ workflow_type = task.workflow_type.name if task.workflow_type else "unknown"
161
+
162
+ # Log task failure with full context (matches Java error logging)
163
+ logger.error(
164
+ "Decision task processing failure",
165
+ extra={
166
+ "workflow_type": workflow_type,
167
+ "workflow_id": workflow_id,
168
+ "run_id": run_id,
169
+ "started_event_id": task.started_event_id,
170
+ "attempt": task.attempt,
171
+ "error_type": type(error).__name__,
172
+ "error_message": str(error),
173
+ },
174
+ exc_info=True,
175
+ )
176
+
177
+ # Determine the failure cause
178
+ cause = DecisionTaskFailedCause.DECISION_TASK_FAILED_CAUSE_UNHANDLED_DECISION
179
+ if isinstance(error, KeyError):
180
+ cause = DecisionTaskFailedCause.DECISION_TASK_FAILED_CAUSE_WORKFLOW_WORKER_UNHANDLED_FAILURE
181
+ elif isinstance(error, ValueError):
182
+ cause = DecisionTaskFailedCause.DECISION_TASK_FAILED_CAUSE_BAD_SCHEDULE_ACTIVITY_ATTRIBUTES
183
+
184
+ # Create error details
185
+ # TODO: Use a data converter for error details serialization
186
+ error_message = str(error).encode("utf-8")
187
+ details = Payload(data=error_message)
188
+
189
+ # Respond with failure
190
+ try:
191
+ await self._client.worker_stub.RespondDecisionTaskFailed(
192
+ RespondDecisionTaskFailedRequest(
193
+ task_token=task.task_token,
194
+ cause=cause,
195
+ identity=self._identity,
196
+ details=details,
197
+ )
198
+ )
199
+ logger.info(
200
+ "Decision task failure response sent",
201
+ extra={
202
+ "workflow_id": workflow_id,
203
+ "run_id": run_id,
204
+ "cause": cause,
205
+ "task_token": task.task_token[:16].hex()
206
+ if task.task_token
207
+ else None,
208
+ },
209
+ )
210
+ except Exception as e:
211
+ logger.error(
212
+ "Error responding to decision task failure",
213
+ extra={
214
+ "workflow_id": workflow_id,
215
+ "run_id": run_id,
216
+ "original_error": type(error).__name__,
217
+ "response_error": type(e).__name__,
218
+ },
219
+ exc_info=True,
220
+ )
221
+
222
+ async def _respond_decision_task_completed(
223
+ self, task: PollForDecisionTaskResponse, decision_result: DecisionResult
224
+ ) -> None:
225
+ """
226
+ Respond to the service that the decision task has been completed.
227
+
228
+ Args:
229
+ task: The original decision task
230
+ decision_result: The result containing decisions and query results
231
+ """
232
+ try:
233
+ request = RespondDecisionTaskCompletedRequest(
234
+ task_token=task.task_token,
235
+ decisions=decision_result.decisions,
236
+ identity=self._identity,
237
+ return_new_decision_task=True,
238
+ )
239
+
240
+ await self._client.worker_stub.RespondDecisionTaskCompleted(request)
241
+
242
+ # Log completion response (matches Java ReplayDecisionTaskHandler trace/debug patterns)
243
+ workflow_execution = task.workflow_execution
244
+ logger.debug(
245
+ "Decision task completion response sent",
246
+ extra={
247
+ "workflow_type": task.workflow_type.name
248
+ if task.workflow_type
249
+ else "unknown",
250
+ "workflow_id": workflow_execution.workflow_id
251
+ if workflow_execution
252
+ else "unknown",
253
+ "run_id": workflow_execution.run_id
254
+ if workflow_execution
255
+ else "unknown",
256
+ "started_event_id": task.started_event_id,
257
+ "decisions_count": len(decision_result.decisions),
258
+ "return_new_decision_task": True,
259
+ "task_token": task.task_token[:16].hex()
260
+ if task.task_token
261
+ else None,
262
+ },
263
+ )
264
+
265
+ except Exception as e:
266
+ workflow_execution = task.workflow_execution
267
+ logger.error(
268
+ "Error responding to decision task completion",
269
+ extra={
270
+ "workflow_type": task.workflow_type.name
271
+ if task.workflow_type
272
+ else "unknown",
273
+ "workflow_id": workflow_execution.workflow_id
274
+ if workflow_execution
275
+ else "unknown",
276
+ "run_id": workflow_execution.run_id
277
+ if workflow_execution
278
+ else "unknown",
279
+ "started_event_id": task.started_event_id,
280
+ "decisions_count": len(decision_result.decisions),
281
+ "error_type": type(e).__name__,
282
+ },
283
+ exc_info=True,
284
+ )
285
+ raise
@@ -0,0 +1,64 @@
1
+ import asyncio
2
+ import logging
3
+ from typing import Callable, TypeVar, Generic, Awaitable, Optional
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+ T = TypeVar("T")
8
+
9
+
10
+ class Poller(Generic[T]):
11
+ def __init__(
12
+ self,
13
+ num_tasks: int,
14
+ permits: asyncio.Semaphore,
15
+ poll: Callable[[], Awaitable[Optional[T]]],
16
+ callback: Callable[[T], Awaitable[None]],
17
+ ) -> None:
18
+ self._num_tasks = num_tasks
19
+ self._permits = permits
20
+ self._poll = poll
21
+ self._callback = callback
22
+ self._background_tasks: set[asyncio.Task[None]] = set()
23
+
24
+ async def run(self) -> None:
25
+ try:
26
+ async with asyncio.TaskGroup() as tg:
27
+ for i in range(self._num_tasks):
28
+ tg.create_task(self._poll_loop())
29
+ except asyncio.CancelledError:
30
+ pass
31
+
32
+ async def _poll_loop(self) -> None:
33
+ while True:
34
+ try:
35
+ await self._poll_and_dispatch()
36
+ except asyncio.CancelledError as e:
37
+ raise e
38
+ except Exception:
39
+ logger.exception("Exception while polling")
40
+
41
+ async def _poll_and_dispatch(self) -> None:
42
+ await self._permits.acquire()
43
+ try:
44
+ task = await self._poll()
45
+ except Exception as e:
46
+ self._permits.release()
47
+ raise e
48
+
49
+ if task is None:
50
+ self._permits.release()
51
+ return
52
+
53
+ # Need to store a reference to the async task or it may be garbage collected
54
+ scheduled = asyncio.create_task(self._execute_callback(task))
55
+ self._background_tasks.add(scheduled)
56
+ scheduled.add_done_callback(self._background_tasks.remove)
57
+
58
+ async def _execute_callback(self, task: T) -> None:
59
+ try:
60
+ await self._callback(task)
61
+ except Exception:
62
+ logger.exception("Exception during callback")
63
+ finally:
64
+ self._permits.release()
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Workflow and Activity Registry for Cadence Python Client.
4
+
5
+ This module provides a registry system for managing workflows and activities,
6
+ similar to the Go client's registry.go implementation.
7
+ """
8
+
9
+ import logging
10
+ from typing import (
11
+ Callable,
12
+ Dict,
13
+ Optional,
14
+ Unpack,
15
+ TypedDict,
16
+ overload,
17
+ Type,
18
+ Union,
19
+ TypeVar,
20
+ )
21
+ from cadence.activity import (
22
+ ActivityDefinitionOptions,
23
+ ActivityDefinition,
24
+ ActivityDecorator,
25
+ P,
26
+ T,
27
+ )
28
+ from cadence.workflow import WorkflowDefinition, WorkflowDefinitionOptions
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ # TypeVar for workflow class types
33
+ W = TypeVar("W")
34
+
35
+
36
+ class RegisterWorkflowOptions(TypedDict, total=False):
37
+ """Options for registering a workflow."""
38
+
39
+ name: Optional[str]
40
+ alias: Optional[str]
41
+
42
+
43
+ class Registry:
44
+ """
45
+ Registry for managing workflows and activities.
46
+
47
+ This class provides functionality to register, retrieve, and manage
48
+ workflows and activities in a Cadence application.
49
+ """
50
+
51
+ def __init__(self) -> None:
52
+ """Initialize the registry."""
53
+ self._workflows: Dict[str, WorkflowDefinition] = {}
54
+ self._activities: Dict[str, ActivityDefinition] = {}
55
+ self._workflow_aliases: Dict[str, str] = {} # alias -> name mapping
56
+
57
+ def workflow(
58
+ self, cls: Optional[Type[W]] = None, **kwargs: Unpack[RegisterWorkflowOptions]
59
+ ) -> Union[Type[W], Callable[[Type[W]], Type[W]]]:
60
+ """
61
+ Register a workflow class.
62
+
63
+ This method can be used as a decorator or called directly.
64
+ Only supports class-based workflows.
65
+
66
+ Args:
67
+ cls: The workflow class to register
68
+ **kwargs: Options for registration (name, alias)
69
+
70
+ Returns:
71
+ The decorated class
72
+
73
+ Raises:
74
+ KeyError: If workflow name already exists
75
+ ValueError: If class workflow is invalid
76
+ """
77
+ options = RegisterWorkflowOptions(**kwargs)
78
+
79
+ def decorator(target: Type[W]) -> Type[W]:
80
+ workflow_name = options.get("name") or target.__name__
81
+
82
+ if workflow_name in self._workflows:
83
+ raise KeyError(f"Workflow '{workflow_name}' is already registered")
84
+
85
+ # Create WorkflowDefinition with type information
86
+ workflow_opts = WorkflowDefinitionOptions(name=workflow_name)
87
+ workflow_def = WorkflowDefinition.wrap(target, workflow_opts)
88
+ self._workflows[workflow_name] = workflow_def
89
+
90
+ # Register alias if provided
91
+ alias = options.get("alias")
92
+ if alias:
93
+ if alias in self._workflow_aliases:
94
+ raise KeyError(f"Workflow alias '{alias}' is already registered")
95
+ self._workflow_aliases[alias] = workflow_name
96
+
97
+ logger.info(f"Registered workflow '{workflow_name}'")
98
+ return target
99
+
100
+ if cls is None:
101
+ return decorator
102
+ return decorator(cls)
103
+
104
+ @overload
105
+ def activity(self, func: Callable[P, T]) -> ActivityDefinition[P, T]: ...
106
+
107
+ @overload
108
+ def activity(
109
+ self, **kwargs: Unpack[ActivityDefinitionOptions]
110
+ ) -> ActivityDecorator: ...
111
+
112
+ def activity(
113
+ self,
114
+ func: Callable[P, T] | None = None,
115
+ **kwargs: Unpack[ActivityDefinitionOptions],
116
+ ) -> ActivityDecorator | ActivityDefinition[P, T]:
117
+ """
118
+ Register an activity function.
119
+
120
+ This method can be used as a decorator or called directly.
121
+
122
+ Args:
123
+ func: The activity function to register
124
+ **kwargs: Options for registration (name, alias)
125
+
126
+ Returns:
127
+ The decorated function or the function itself
128
+
129
+ Raises:
130
+ KeyError: If activity name already exists
131
+ """
132
+ options = ActivityDefinitionOptions(**kwargs)
133
+
134
+ def decorator(f: Callable[P, T]) -> ActivityDefinition[P, T]:
135
+ defn = ActivityDefinition.wrap(f, options)
136
+
137
+ self._register_activity(defn)
138
+
139
+ return defn
140
+
141
+ if func is not None:
142
+ return decorator(func)
143
+
144
+ return decorator
145
+
146
+ def register_activities(self, obj: object) -> None:
147
+ activities = _find_activity_definitions(obj)
148
+ if not activities:
149
+ raise ValueError(f"No activity definitions found in '{repr(obj)}'")
150
+
151
+ for defn in activities:
152
+ self._register_activity(defn)
153
+
154
+ def register_activity(self, defn: Callable) -> None:
155
+ if not isinstance(defn, ActivityDefinition):
156
+ raise ValueError(f"{defn.__qualname__} must have @activity.defn decorator")
157
+ self._register_activity(defn)
158
+
159
+ def _register_activity(self, defn: ActivityDefinition) -> None:
160
+ if defn.name in self._activities:
161
+ raise KeyError(f"Activity '{defn.name}' is already registered")
162
+
163
+ self._activities[defn.name] = defn
164
+
165
+ def get_workflow(self, name: str) -> WorkflowDefinition:
166
+ """
167
+ Get a registered workflow by name.
168
+
169
+ Args:
170
+ name: Name or alias of the workflow
171
+
172
+ Returns:
173
+ The workflow definition
174
+
175
+ Raises:
176
+ KeyError: If workflow is not found
177
+ """
178
+ # Check if it's an alias
179
+ actual_name = self._workflow_aliases.get(name, name)
180
+
181
+ if actual_name not in self._workflows:
182
+ raise KeyError(f"Workflow '{name}' not found in registry")
183
+
184
+ return self._workflows[actual_name]
185
+
186
+ def get_activity(self, name: str) -> ActivityDefinition:
187
+ """
188
+ Get a registered activity by name.
189
+
190
+ Args:
191
+ name: Name or alias of the activity
192
+
193
+ Returns:
194
+ The activity function
195
+
196
+ Raises:
197
+ KeyError: If activity is not found
198
+ """
199
+ return self._activities[name]
200
+
201
+ def __add__(self, other: "Registry") -> "Registry":
202
+ result = Registry()
203
+ for name, fn in self._activities.items():
204
+ result._register_activity(fn)
205
+ for name, fn in other._activities.items():
206
+ result._register_activity(fn)
207
+
208
+ return result
209
+
210
+ @staticmethod
211
+ def of(*args: "Registry") -> "Registry":
212
+ result = Registry()
213
+ for other in args:
214
+ result += other
215
+
216
+ return result
217
+
218
+
219
+ def _find_activity_definitions(instance: object) -> list[ActivityDefinition]:
220
+ attr_to_def: dict[str, ActivityDefinition] = {}
221
+ for t in instance.__class__.__mro__:
222
+ for attr in dir(t):
223
+ if attr.startswith("_"):
224
+ continue
225
+ value = getattr(t, attr)
226
+ if isinstance(value, ActivityDefinition):
227
+ if attr in attr_to_def:
228
+ raise ValueError(
229
+ f"'{attr}' was overridden with a duplicate activity definition"
230
+ )
231
+ attr_to_def[attr] = value
232
+
233
+ result: list[ActivityDefinition] = []
234
+ for attr, definition in attr_to_def.items():
235
+ result.append(
236
+ ActivityDefinition(
237
+ getattr(instance, attr),
238
+ definition.name,
239
+ definition.strategy,
240
+ definition.params,
241
+ definition.result_type,
242
+ )
243
+ )
244
+
245
+ return result
@@ -0,0 +1,26 @@
1
+ from typing import TypedDict
2
+
3
+
4
+ class WorkerOptions(TypedDict, total=False):
5
+ max_concurrent_activity_execution_size: int
6
+ max_concurrent_decision_task_execution_size: int
7
+ task_list_activities_per_second: float
8
+ # Remove these in favor of introducing automatic scaling prior to release
9
+ activity_task_pollers: int
10
+ decision_task_pollers: int
11
+ disable_workflow_worker: bool
12
+ disable_activity_worker: bool
13
+ identity: str
14
+
15
+
16
+ _DEFAULT_WORKER_OPTIONS: WorkerOptions = {
17
+ "max_concurrent_activity_execution_size": 1000,
18
+ "max_concurrent_decision_task_execution_size": 1000,
19
+ "task_list_activities_per_second": 0.0,
20
+ "activity_task_pollers": 2,
21
+ "decision_task_pollers": 2,
22
+ "disable_workflow_worker": False,
23
+ "disable_activity_worker": False,
24
+ }
25
+
26
+ _LONG_POLL_TIMEOUT = 60.0
@@ -0,0 +1,56 @@
1
+ import asyncio
2
+ import uuid
3
+ from typing import Unpack, cast
4
+
5
+ from cadence.client import Client
6
+ from cadence.worker._registry import Registry
7
+ from cadence.worker._activity import ActivityWorker
8
+ from cadence.worker._decision import DecisionWorker
9
+ from cadence.worker._types import WorkerOptions, _DEFAULT_WORKER_OPTIONS
10
+
11
+
12
+ class Worker:
13
+ def __init__(
14
+ self,
15
+ client: Client,
16
+ task_list: str,
17
+ registry: Registry,
18
+ **kwargs: Unpack[WorkerOptions],
19
+ ) -> None:
20
+ self._client = client
21
+ self._task_list = task_list
22
+
23
+ options = WorkerOptions(**kwargs)
24
+ _validate_and_copy_defaults(client, task_list, options)
25
+ self._options = options
26
+ self._activity_worker = ActivityWorker(client, task_list, registry, options)
27
+ self._decision_worker = DecisionWorker(client, task_list, registry, options)
28
+
29
+ @property
30
+ def client(self) -> Client:
31
+ return self._client
32
+
33
+ @property
34
+ def task_list(self) -> str:
35
+ return self._task_list
36
+
37
+ async def run(self) -> None:
38
+ async with asyncio.TaskGroup() as tg:
39
+ if not self._options["disable_workflow_worker"]:
40
+ tg.create_task(self._decision_worker.run())
41
+ if not self._options["disable_activity_worker"]:
42
+ tg.create_task(self._activity_worker.run())
43
+
44
+
45
+ def _validate_and_copy_defaults(
46
+ client: Client, task_list: str, options: WorkerOptions
47
+ ) -> None:
48
+ if "identity" not in options:
49
+ options["identity"] = f"{client.identity}@{task_list}@{uuid.uuid4()}"
50
+
51
+ # TODO: More validation
52
+
53
+ # Set default values for missing options
54
+ for key, value in _DEFAULT_WORKER_OPTIONS.items():
55
+ if key not in options:
56
+ cast(dict, options)[key] = value