cadence-python-client 0.1.0__tar.gz → 0.2.0__tar.gz

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 (108) hide show
  1. {cadence_python_client-0.1.0/cadence_python_client.egg-info → cadence_python_client-0.2.0}/PKG-INFO +5 -5
  2. cadence_python_client-0.2.0/cadence/_internal/activity/__init__.py +10 -0
  3. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/_internal/activity/_activity_executor.py +5 -3
  4. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/_internal/activity/_context.py +11 -9
  5. cadence_python_client-0.2.0/cadence/_internal/activity/_definition.py +178 -0
  6. cadence_python_client-0.2.0/cadence/_internal/fn_signature.py +90 -0
  7. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/_internal/rpc/yarpc.py +3 -0
  8. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/_internal/workflow/context.py +0 -4
  9. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/_internal/workflow/deterministic_event_loop.py +37 -3
  10. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/_internal/workflow/statemachine/activity_state_machine.py +14 -9
  11. cadence_python_client-0.2.0/cadence/_internal/workflow/statemachine/cancellation.py +36 -0
  12. cadence_python_client-0.2.0/cadence/_internal/workflow/statemachine/completion_state_machine.py +25 -0
  13. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/_internal/workflow/statemachine/decision_manager.py +72 -13
  14. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/_internal/workflow/statemachine/decision_state_machine.py +31 -13
  15. cadence_python_client-0.2.0/cadence/_internal/workflow/statemachine/nondeterminism.py +308 -0
  16. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/_internal/workflow/statemachine/timer_state_machine.py +13 -8
  17. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/_internal/workflow/workflow_engine.py +96 -100
  18. cadence_python_client-0.2.0/cadence/_internal/workflow/workflow_instance.py +53 -0
  19. cadence_python_client-0.2.0/cadence/activity.py +342 -0
  20. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/common_pb2.py +3 -3
  21. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/common_pb2.pyi +4 -2
  22. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/common_pb2_grpc.py +1 -1
  23. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/decision_pb2.py +3 -3
  24. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/decision_pb2.pyi +7 -5
  25. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/decision_pb2_grpc.py +1 -1
  26. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/domain_pb2.py +3 -3
  27. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/domain_pb2.pyi +6 -4
  28. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/domain_pb2_grpc.py +1 -1
  29. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/error_pb2.py +3 -3
  30. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/error_pb2.pyi +2 -1
  31. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/error_pb2_grpc.py +1 -1
  32. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/history_pb2.py +3 -3
  33. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/history_pb2.pyi +10 -8
  34. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/history_pb2_grpc.py +1 -1
  35. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/query_pb2.py +3 -3
  36. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/query_pb2.pyi +2 -1
  37. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/query_pb2_grpc.py +1 -1
  38. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/service_domain_pb2.py +3 -3
  39. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/service_domain_pb2.pyi +5 -3
  40. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/service_domain_pb2_grpc.py +1 -1
  41. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/service_meta_pb2.py +3 -3
  42. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/service_meta_pb2_grpc.py +1 -1
  43. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/service_visibility_pb2.py +3 -3
  44. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/service_visibility_pb2.pyi +2 -1
  45. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/service_visibility_pb2_grpc.py +1 -1
  46. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/service_worker_pb2.py +3 -3
  47. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/service_worker_pb2.pyi +5 -3
  48. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/service_worker_pb2_grpc.py +1 -1
  49. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/service_workflow_pb2.py +3 -3
  50. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/service_workflow_pb2.pyi +4 -2
  51. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/service_workflow_pb2_grpc.py +1 -1
  52. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/tasklist_pb2.py +3 -3
  53. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/tasklist_pb2.pyi +5 -3
  54. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/tasklist_pb2_grpc.py +1 -1
  55. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/visibility_pb2.py +3 -3
  56. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/visibility_pb2.pyi +4 -2
  57. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/visibility_pb2_grpc.py +1 -1
  58. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/workflow_pb2.py +3 -3
  59. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/workflow_pb2.pyi +9 -7
  60. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/workflow_pb2_grpc.py +1 -1
  61. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/client.py +55 -1
  62. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/data_converter.py +9 -5
  63. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/worker/__init__.py +0 -2
  64. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/worker/_registry.py +67 -81
  65. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/workflow.py +26 -2
  66. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0/cadence_python_client.egg-info}/PKG-INFO +5 -5
  67. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence_python_client.egg-info/SOURCES.txt +6 -2
  68. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence_python_client.egg-info/requires.txt +4 -4
  69. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/pyproject.toml +6 -6
  70. cadence_python_client-0.1.0/cadence/_internal/activity/__init__.py +0 -5
  71. cadence_python_client-0.1.0/cadence/_internal/workflow/decisions_helper.py +0 -312
  72. cadence_python_client-0.1.0/cadence/_internal/workflow/workflow_intance.py +0 -44
  73. cadence_python_client-0.1.0/cadence/activity.py +0 -255
  74. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/LICENSE +0 -0
  75. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/NOTICE +0 -0
  76. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/README.md +0 -0
  77. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/__init__.py +0 -0
  78. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/_internal/__init__.py +0 -0
  79. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/_internal/rpc/__init__.py +0 -0
  80. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/_internal/rpc/error.py +0 -0
  81. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/_internal/rpc/retry.py +0 -0
  82. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/_internal/workflow/__init__.py +0 -0
  83. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/_internal/workflow/decision_events_iterator.py +0 -0
  84. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/_internal/workflow/history_event_iterator.py +0 -0
  85. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/_internal/workflow/statemachine/__init__.py +0 -0
  86. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/_internal/workflow/statemachine/event_dispatcher.py +0 -0
  87. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/__init__.py +0 -0
  88. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/api/v1/service_meta_pb2.pyi +0 -0
  89. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/error.py +0 -0
  90. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/metrics/__init__.py +0 -0
  91. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/metrics/constants.py +0 -0
  92. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/metrics/metrics.py +0 -0
  93. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/metrics/prometheus.py +0 -0
  94. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/sample/__init__.py +0 -0
  95. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/sample/client_example.py +0 -0
  96. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/sample/grpc_usage_example.py +0 -0
  97. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/sample/simple_usage_example.py +0 -0
  98. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/signal.py +0 -0
  99. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/worker/_activity.py +0 -0
  100. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/worker/_base_task_handler.py +0 -0
  101. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/worker/_decision.py +0 -0
  102. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/worker/_decision_task_handler.py +0 -0
  103. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/worker/_poller.py +0 -0
  104. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/worker/_types.py +0 -0
  105. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence/worker/_worker.py +0 -0
  106. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence_python_client.egg-info/dependency_links.txt +0 -0
  107. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/cadence_python_client.egg-info/top_level.txt +0 -0
  108. {cadence_python_client-0.1.0 → cadence_python_client-0.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cadence-python-client
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Python framework for authoring Cadence workflows and activities
5
5
  Author: Cadence
6
6
  License: Apache-2.0
@@ -23,14 +23,14 @@ Requires-Python: <3.14,>=3.11
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
25
  License-File: NOTICE
26
- Requires-Dist: grpcio==1.71.2
27
- Requires-Dist: grpcio-status>=1.71.2
26
+ Requires-Dist: grpcio>=1.73.1
27
+ Requires-Dist: grpcio-status>=1.73.1
28
28
  Requires-Dist: msgspec>=0.19.0
29
- Requires-Dist: protobuf==5.29.1
29
+ Requires-Dist: protobuf<7.0.0,>=6.31.0
30
30
  Requires-Dist: typing-extensions>=4.0.0
31
31
  Requires-Dist: prometheus-client>=0.21.0
32
32
  Provides-Extra: dev
33
- Requires-Dist: grpcio-tools==1.71.2; extra == "dev"
33
+ Requires-Dist: grpcio-tools>=1.73.1; extra == "dev"
34
34
  Requires-Dist: pytest>=8.4.1; extra == "dev"
35
35
  Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
36
36
  Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
@@ -0,0 +1,10 @@
1
+ from ._activity_executor import ActivityExecutor
2
+ from ._definition import AsyncImpl, AsyncMethodImpl, SyncImpl, SyncMethodImpl
3
+
4
+ __all__ = [
5
+ "ActivityExecutor",
6
+ "AsyncImpl",
7
+ "AsyncMethodImpl",
8
+ "SyncImpl",
9
+ "SyncMethodImpl",
10
+ ]
@@ -1,12 +1,13 @@
1
1
  from concurrent.futures import ThreadPoolExecutor
2
2
  from logging import getLogger
3
3
  from traceback import format_exception
4
- from typing import Any, Callable
4
+ from typing import Any, Callable, cast
5
5
  from google.protobuf.duration import to_timedelta
6
6
  from google.protobuf.timestamp import to_datetime
7
7
 
8
8
  from cadence._internal.activity._context import _Context, _SyncContext
9
- from cadence.activity import ActivityInfo, ActivityDefinition, ExecutionStrategy
9
+ from cadence._internal.activity._definition import BaseDefinition, ExecutionStrategy
10
+ from cadence.activity import ActivityInfo, ActivityDefinition
10
11
  from cadence.api.v1.common_pb2 import Failure
11
12
  from cadence.api.v1.service_worker_pb2 import (
12
13
  PollForActivityTaskResponse,
@@ -42,12 +43,13 @@ class ActivityExecutor:
42
43
  result = await context.execute(task.input)
43
44
  await self._report_success(task, result)
44
45
  except Exception as e:
46
+ _logger.exception("Activity failed")
45
47
  await self._report_failure(task, e)
46
48
 
47
49
  def _create_context(self, task: PollForActivityTaskResponse) -> _Context:
48
50
  activity_type = task.activity_type.name
49
51
  try:
50
- activity_def = self._registry(activity_type)
52
+ activity_def = cast(BaseDefinition, self._registry(activity_type))
51
53
  except KeyError:
52
54
  raise KeyError(f"Activity type not found: {activity_type}") from None
53
55
 
@@ -3,7 +3,8 @@ from concurrent.futures.thread import ThreadPoolExecutor
3
3
  from typing import Any
4
4
 
5
5
  from cadence import Client
6
- from cadence.activity import ActivityInfo, ActivityContext, ActivityDefinition
6
+ from cadence._internal.activity._definition import BaseDefinition
7
+ from cadence.activity import ActivityInfo, ActivityContext
7
8
  from cadence.api.v1.common_pb2 import Payload
8
9
 
9
10
 
@@ -12,20 +13,21 @@ class _Context(ActivityContext):
12
13
  self,
13
14
  client: Client,
14
15
  info: ActivityInfo,
15
- activity_fn: ActivityDefinition[[Any], Any],
16
+ activity_def: BaseDefinition[[Any], Any],
16
17
  ):
17
18
  self._client = client
18
19
  self._info = info
19
- self._activity_fn = activity_fn
20
+ self._activity_def = activity_def
20
21
 
21
22
  async def execute(self, payload: Payload) -> Any:
22
23
  params = self._to_params(payload)
23
24
  with self._activate():
24
- return await self._activity_fn(*params)
25
+ return await self._activity_def.impl_fn(*params)
25
26
 
26
27
  def _to_params(self, payload: Payload) -> list[Any]:
27
- type_hints = [param.type_hint for param in self._activity_fn.params]
28
- return self._client.data_converter.from_data(payload, type_hints)
28
+ return self._activity_def.signature.params_from_payload(
29
+ self._client.data_converter, payload
30
+ )
29
31
 
30
32
  def client(self) -> Client:
31
33
  return self._client
@@ -39,10 +41,10 @@ class _SyncContext(_Context):
39
41
  self,
40
42
  client: Client,
41
43
  info: ActivityInfo,
42
- activity_fn: ActivityDefinition[[Any], Any],
44
+ activity_def: BaseDefinition[[Any], Any],
43
45
  executor: ThreadPoolExecutor,
44
46
  ):
45
- super().__init__(client, info, activity_fn)
47
+ super().__init__(client, info, activity_def)
46
48
  self._executor = executor
47
49
 
48
50
  async def execute(self, payload: Payload) -> Any:
@@ -52,7 +54,7 @@ class _SyncContext(_Context):
52
54
 
53
55
  def _run(self, args: list[Any]) -> Any:
54
56
  with self._activate():
55
- return self._activity_fn(*args)
57
+ return self._activity_def.impl_fn(*args)
56
58
 
57
59
  def client(self) -> Client:
58
60
  raise RuntimeError("client is only supported in async activities")
@@ -0,0 +1,178 @@
1
+ import abc
2
+ from abc import ABC
3
+ from enum import Enum
4
+ from functools import update_wrapper, partial
5
+ from typing import (
6
+ Generic,
7
+ Callable,
8
+ Unpack,
9
+ Self,
10
+ ParamSpec,
11
+ TypeVar,
12
+ Awaitable,
13
+ cast,
14
+ Concatenate,
15
+ )
16
+
17
+ from cadence._internal.fn_signature import FnSignature
18
+ from cadence.workflow import ActivityOptions, WorkflowContext, execute_activity
19
+
20
+ T = TypeVar("T")
21
+ P = ParamSpec("P")
22
+ R = TypeVar("R")
23
+
24
+
25
+ class ExecutionStrategy(Enum):
26
+ ASYNC = "async"
27
+ THREAD_POOL = "thread_pool"
28
+
29
+
30
+ class BaseDefinition(ABC, Generic[P, R]):
31
+ def __init__(
32
+ self,
33
+ name: str,
34
+ wrapped: Callable,
35
+ strategy: ExecutionStrategy,
36
+ signature: FnSignature,
37
+ ):
38
+ self._name = name
39
+ self._wrapped = wrapped
40
+ self._strategy = strategy
41
+ self._signature = signature
42
+ self._execution_options = ActivityOptions()
43
+
44
+ @property
45
+ def strategy(self) -> ExecutionStrategy:
46
+ return self._strategy
47
+
48
+ @property
49
+ def signature(self) -> FnSignature:
50
+ return self._signature
51
+
52
+ @property
53
+ def impl_fn(self) -> Callable:
54
+ return self._wrapped
55
+
56
+ @property
57
+ def name(self) -> str:
58
+ return self._name
59
+
60
+ @abc.abstractmethod
61
+ def clone(self) -> Self: ...
62
+
63
+ def rebind(self, fn: Callable) -> Self:
64
+ res = self.clone()
65
+ res._wrapped = fn
66
+ return res
67
+
68
+ def with_options(self, **kwargs: Unpack[ActivityOptions]) -> Self:
69
+ res = self.clone()
70
+ new_opts = self._execution_options.copy()
71
+ new_opts.update(kwargs)
72
+ res._execution_options = new_opts
73
+ return res
74
+
75
+ async def execute(self, *args: P.args, **kwargs: P.kwargs) -> R:
76
+ result_type = cast(type[R], self._signature.return_type)
77
+ return await execute_activity(
78
+ self._name,
79
+ result_type,
80
+ *self._signature.params_from_call(args, kwargs),
81
+ **self._execution_options,
82
+ )
83
+
84
+
85
+ class SyncImpl(BaseDefinition[P, R]):
86
+ def __init__(
87
+ self,
88
+ wrapped: Callable[P, R],
89
+ name: str,
90
+ signature: FnSignature,
91
+ ):
92
+ super().__init__(name, wrapped, ExecutionStrategy.THREAD_POOL, signature)
93
+ update_wrapper(self, wrapped)
94
+
95
+ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
96
+ if WorkflowContext.is_set():
97
+ raise RuntimeError(
98
+ "Attempting to invoke sync function in workflow. Use execute"
99
+ )
100
+ return self._wrapped(*args, **kwargs) # type: ignore
101
+
102
+ def clone(self) -> "SyncImpl[P, R]":
103
+ return SyncImpl[P, R](self._wrapped, self._name, self._signature)
104
+
105
+
106
+ class SyncMethodImpl(BaseDefinition[P, R], Generic[T, P, R]):
107
+ def __init__(
108
+ self,
109
+ wrapped: Callable[Concatenate[T, P], R],
110
+ name: str,
111
+ signature: FnSignature,
112
+ ):
113
+ super().__init__(name, wrapped, ExecutionStrategy.THREAD_POOL, signature)
114
+ update_wrapper(self, wrapped)
115
+
116
+ def __get__(self, instance, owner):
117
+ if instance is None:
118
+ return self
119
+ # If we bound the method to an instance, then drop the self parameter. It's a normal function again
120
+ return SyncImpl[P, R](
121
+ partial(self._wrapped, instance), self.name, self._signature
122
+ )
123
+
124
+ def __call__(self, original_self: T, *args: P.args, **kwargs: P.kwargs) -> R:
125
+ if WorkflowContext.is_set():
126
+ raise RuntimeError(
127
+ "Attempting to invoke sync function in workflow. Use execute"
128
+ )
129
+ return self._wrapped(original_self, *args, **kwargs) # type: ignore
130
+
131
+ def clone(self) -> "SyncMethodImpl[T, P, R]":
132
+ return SyncMethodImpl[T, P, R](self._wrapped, self._name, self._signature)
133
+
134
+
135
+ class AsyncImpl(BaseDefinition[P, R]):
136
+ def __init__(
137
+ self,
138
+ wrapped: Callable[P, Awaitable[R]],
139
+ name: str,
140
+ signature: FnSignature,
141
+ ):
142
+ super().__init__(name, wrapped, ExecutionStrategy.ASYNC, signature)
143
+ update_wrapper(self, wrapped)
144
+
145
+ async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
146
+ if WorkflowContext.is_set():
147
+ return await self.execute(*args, **kwargs) # type: ignore
148
+ return await self._wrapped(*args, **kwargs) # type: ignore
149
+
150
+ def clone(self) -> "AsyncImpl[P, R]":
151
+ return AsyncImpl[P, R](self._wrapped, self._name, self._signature)
152
+
153
+
154
+ class AsyncMethodImpl(BaseDefinition[P, R], Generic[T, P, R]):
155
+ def __init__(
156
+ self,
157
+ wrapped: Callable[Concatenate[T, P], Awaitable[R]],
158
+ name: str,
159
+ signature: FnSignature,
160
+ ):
161
+ super().__init__(name, wrapped, ExecutionStrategy.ASYNC, signature)
162
+ update_wrapper(self, wrapped)
163
+
164
+ def __get__(self, instance, owner):
165
+ if instance is None:
166
+ return self
167
+ # If we bound the method to an instance, then drop the self parameter. It's a normal function again
168
+ return AsyncImpl[P, R](
169
+ partial(self._wrapped, instance), self.name, self._signature
170
+ )
171
+
172
+ async def __call__(self, original_self: T, *args: P.args, **kwargs: P.kwargs) -> R:
173
+ if WorkflowContext.is_set():
174
+ return await self.execute(*args, **kwargs) # type: ignore
175
+ return await self._wrapped(original_self, *args, **kwargs) # type: ignore
176
+
177
+ def clone(self) -> "AsyncMethodImpl[T, P, R]":
178
+ return AsyncMethodImpl[T, P, R](self._wrapped, self._name, self._signature)
@@ -0,0 +1,90 @@
1
+ from dataclasses import dataclass
2
+ from inspect import signature, Parameter
3
+ from typing import (
4
+ Type,
5
+ Any,
6
+ Callable,
7
+ Sequence,
8
+ get_type_hints,
9
+ )
10
+
11
+ from cadence.api.v1.common_pb2 import Payload
12
+ from cadence.data_converter import DataConverter
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class FnParameter:
17
+ name: str
18
+ type_hint: Type | None
19
+ has_default: bool = False
20
+ default_value: Any = None
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class FnSignature:
25
+ params: list[FnParameter]
26
+ return_type: Type
27
+
28
+ def params_from_call(
29
+ self, args: Sequence[Any], kwargs: dict[str, Any]
30
+ ) -> list[Any]:
31
+ result: list[Any] = []
32
+ if len(args) > len(self.params):
33
+ raise ValueError(f"Too many positional arguments: {args}")
34
+
35
+ for value, param_spec in zip(args, self.params):
36
+ result.append(value)
37
+
38
+ i = len(result)
39
+ while i < len(self.params):
40
+ param = self.params[i]
41
+ if param.name not in kwargs and not param.has_default:
42
+ raise ValueError(f"Missing parameter: {param.name}")
43
+
44
+ value = kwargs.pop(param.name, param.default_value)
45
+ result.append(value)
46
+ i = i + 1
47
+
48
+ if len(kwargs) > 0:
49
+ raise ValueError(f"Unexpected keyword arguments: {kwargs}")
50
+
51
+ return result
52
+
53
+ def params_from_payload(
54
+ self, data_converter: DataConverter, payload: Payload
55
+ ) -> list[Any]:
56
+ type_hints = [param.type_hint for param in self.params]
57
+ return data_converter.from_data(payload, type_hints)
58
+
59
+ @staticmethod
60
+ def of(fn: Callable) -> "FnSignature":
61
+ sig = signature(fn)
62
+ args = sig.parameters
63
+ hints = get_type_hints(fn)
64
+ params = []
65
+ for name, param in args.items():
66
+ # "unbound functions" aren't a thing in the Python spec. We don't have a way to determine whether the function
67
+ # is part of a class or is standalone.
68
+ # Filter out the self parameter and hope they followed the convention.
69
+ if param.name == "self":
70
+ continue
71
+ default = None
72
+ has_default = False
73
+ if param.default != Parameter.empty:
74
+ default = param.default
75
+ has_default = param.default is not None
76
+ if param.kind in (
77
+ Parameter.POSITIONAL_ONLY,
78
+ Parameter.POSITIONAL_OR_KEYWORD,
79
+ ):
80
+ type_hint = hints.get(name, None)
81
+ params.append(FnParameter(name, type_hint, has_default, default))
82
+ else:
83
+ raise ValueError(
84
+ f"Parameters must be positional. {name} is {param.kind}, and not valid"
85
+ )
86
+
87
+ # Treat unspecified return type as Any
88
+ return_type = hints.get("return", Any)
89
+
90
+ return FnSignature(params, return_type)
@@ -8,6 +8,8 @@ SERVICE_KEY = "rpc-service"
8
8
  CALLER_KEY = "rpc-caller"
9
9
  ENCODING_KEY = "rpc-encoding"
10
10
  ENCODING_PROTO = "proto"
11
+ CALLER_TYPE_KEY = "cadence-caller-type"
12
+ CALLER_TYPE_VALUE = "sdk"
11
13
 
12
14
 
13
15
  class YarpcMetadataInterceptor(UnaryUnaryClientInterceptor):
@@ -16,6 +18,7 @@ class YarpcMetadataInterceptor(UnaryUnaryClientInterceptor):
16
18
  (SERVICE_KEY, service),
17
19
  (CALLER_KEY, caller),
18
20
  (ENCODING_KEY, ENCODING_PROTO),
21
+ (CALLER_TYPE_KEY, CALLER_TYPE_VALUE),
19
22
  )
20
23
 
21
24
  async def intercept_unary_unary(
@@ -4,7 +4,6 @@ from math import ceil
4
4
  from typing import Iterator, Optional, Any, Unpack, Type, cast
5
5
 
6
6
  from cadence._internal.workflow.statemachine.decision_manager import DecisionManager
7
- from cadence._internal.workflow.decisions_helper import DecisionsHelper
8
7
  from cadence.api.v1.common_pb2 import ActivityType
9
8
  from cadence.api.v1.decision_pb2 import ScheduleActivityTaskDecisionAttributes
10
9
  from cadence.api.v1.tasklist_pb2 import TaskList, TaskListKind
@@ -21,7 +20,6 @@ class Context(WorkflowContext):
21
20
  self._info = info
22
21
  self._replay_mode = True
23
22
  self._replay_current_time_milliseconds: Optional[int] = None
24
- self._decision_helper = DecisionsHelper()
25
23
  self._decision_manager = decision_manager
26
24
 
27
25
  def info(self) -> WorkflowInfo:
@@ -70,9 +68,7 @@ class Context(WorkflowContext):
70
68
  )
71
69
 
72
70
  activity_input = self.data_converter().to_data(list(args))
73
- activity_id = self._decision_helper.generate_activity_id(activity)
74
71
  schedule_attributes = ScheduleActivityTaskDecisionAttributes(
75
- activity_id=activity_id,
76
72
  activity_type=ActivityType(name=activity),
77
73
  domain=self.info().workflow_domain,
78
74
  task_list=TaskList(kind=TaskListKind.TASK_LIST_KIND_NORMAL, name=task_list),
@@ -1,3 +1,4 @@
1
+ import traceback
1
2
  from asyncio import AbstractEventLoop, Handle, TimerHandle, futures, tasks, Future, Task
2
3
  from contextvars import Context
3
4
  import logging
@@ -9,6 +10,12 @@ from typing_extensions import Unpack, TypeVarTuple
9
10
 
10
11
  logger = logging.getLogger(__name__)
11
12
 
13
+
14
+ class FatalDecisionError(Exception):
15
+ def __init__(self, *args) -> None:
16
+ super().__init__(*args)
17
+
18
+
12
19
  _Ts = TypeVarTuple("_Ts")
13
20
  _T = TypeVar("_T")
14
21
 
@@ -455,9 +462,36 @@ class DeterministicEventLoop(AbstractEventLoop):
455
462
  )
456
463
 
457
464
  def call_exception_handler(self, context: dict[str, Any]) -> None:
458
- raise NotImplementedError(
459
- "Custom exception handlers not supported in deterministic event loop"
460
- )
465
+ # This is called if a task has an unhandled exception. Short term, it's helpful to log these for debugging.
466
+ # Long term, we need some combination of failing decision tasks or workflows based on these errors.
467
+ message = context.get("message")
468
+ if not message:
469
+ message = "Unhandled exception in event loop"
470
+
471
+ exception = context.get("exception")
472
+ if isinstance(exception, BaseException):
473
+ exc_info = exception
474
+ else:
475
+ exc_info = None
476
+
477
+ log_lines = [message]
478
+ for key in sorted(context):
479
+ if key in {"message", "exception"}:
480
+ continue
481
+ value = context[key]
482
+ if key == "source_traceback":
483
+ tb = "".join(traceback.format_list(value))
484
+ value = "Object created at (most recent call last):\n"
485
+ value += tb.rstrip()
486
+ elif key == "handle_traceback":
487
+ tb = "".join(traceback.format_list(value))
488
+ value = "Handle created at (most recent call last):\n"
489
+ value += tb.rstrip()
490
+ else:
491
+ value = repr(value)
492
+ log_lines.append(f"{key}: {value}")
493
+
494
+ logger.error("\n".join(log_lines), exc_info=exc_info)
461
495
 
462
496
  # Task factory
463
497
  def set_task_factory( # type: ignore[override]
@@ -6,6 +6,9 @@ from cadence._internal.workflow.statemachine.decision_state_machine import (
6
6
  BaseDecisionStateMachine,
7
7
  )
8
8
  from cadence._internal.workflow.statemachine.event_dispatcher import EventDispatcher
9
+ from cadence._internal.workflow.statemachine.nondeterminism import (
10
+ record_immediate_cancel,
11
+ )
9
12
  from cadence.api.v1 import decision, history
10
13
  from cadence.api.v1.common_pb2 import Payload
11
14
  from cadence.error import ActivityFailure
@@ -30,12 +33,14 @@ class ActivityStateMachine(BaseDecisionStateMachine):
30
33
  return DecisionId(DecisionType.ACTIVITY, self.request.activity_id)
31
34
 
32
35
  def get_decision(self) -> decision.Decision | None:
33
- if self.state is DecisionState.CREATED:
36
+ if self.state is DecisionState.REQUESTED:
34
37
  return decision.Decision(
35
38
  schedule_activity_task_decision_attributes=self.request
36
39
  )
40
+ if self.state is DecisionState.CANCELED_AFTER_REQUESTED:
41
+ return record_immediate_cancel(self.request)
37
42
 
38
- if self.state is DecisionState.CANCELED_AFTER_INITIATED:
43
+ if self.state is DecisionState.CANCELED_AFTER_RECORDED:
39
44
  return decision.Decision(
40
45
  request_cancel_activity_task_decision_attributes=decision.RequestCancelActivityTaskDecisionAttributes(
41
46
  activity_id=self.request.activity_id,
@@ -45,20 +50,20 @@ class ActivityStateMachine(BaseDecisionStateMachine):
45
50
  return None
46
51
 
47
52
  def request_cancel(self) -> bool:
48
- if self.state is DecisionState.CREATED:
49
- self._transition(DecisionState.COMPLETED)
53
+ if self.state is DecisionState.REQUESTED:
54
+ self._transition(DecisionState.CANCELED_AFTER_REQUESTED)
50
55
  self.completed.force_cancel()
51
56
  return True
52
57
 
53
- if self.state is DecisionState.INITIATED:
54
- self._transition(DecisionState.CANCELED_AFTER_INITIATED)
58
+ if self.state is DecisionState.RECORDED:
59
+ self._transition(DecisionState.CANCELED_AFTER_RECORDED)
55
60
  return True
56
61
 
57
62
  return False
58
63
 
59
64
  @activity_events.event(id_attr="activity_id", event_id_is_alias=True)
60
65
  def handle_scheduled(self, _: history.ActivityTaskScheduledEventAttributes) -> None:
61
- self._transition(DecisionState.INITIATED)
66
+ self._transition(DecisionState.RECORDED)
62
67
 
63
68
  @activity_events.event()
64
69
  def handle_started(self, _: history.ActivityTaskStartedEventAttributes) -> None:
@@ -97,10 +102,10 @@ class ActivityStateMachine(BaseDecisionStateMachine):
97
102
  def handle_cancel_requested(
98
103
  self, _: history.ActivityTaskCancelRequestedEventAttributes
99
104
  ) -> None:
100
- self._transition(DecisionState.CANCELLATION_DECISION_SENT)
105
+ self._transition(DecisionState.CANCELLATION_RECORDED)
101
106
 
102
107
  @activity_events.event("activity_id")
103
108
  def handle_cancel_failed(
104
109
  self, _: history.RequestCancelActivityTaskFailedEventAttributes
105
110
  ) -> None:
106
- self._transition(DecisionState.INITIATED)
111
+ self._transition(DecisionState.RECORDED)
@@ -0,0 +1,36 @@
1
+ from typing import Dict, Any, Tuple
2
+
3
+ from cadence._internal.workflow.statemachine.decision_state_machine import (
4
+ DecisionId,
5
+ DecisionType,
6
+ )
7
+
8
+ from cadence.api.v1 import decision, history
9
+ from cadence.api.v1.common_pb2 import Payload
10
+ from msgspec import json
11
+
12
+
13
+ MARKER_PREFIX = "Cancel_"
14
+
15
+
16
+ def is_immediate_cancel(marker: history.MarkerRecordedEventAttributes) -> bool:
17
+ return marker.marker_name.startswith(MARKER_PREFIX)
18
+
19
+
20
+ def to_marker(
21
+ decision_id: DecisionId, props: Dict[str, Any]
22
+ ) -> decision.RecordMarkerDecisionAttributes:
23
+ data = props | {"type": decision_id.decision_type.name}
24
+ return decision.RecordMarkerDecisionAttributes(
25
+ marker_name=MARKER_PREFIX + decision_id.id,
26
+ details=Payload(data=json.encode(data)),
27
+ )
28
+
29
+
30
+ def from_marker(
31
+ marker: history.MarkerRecordedEventAttributes,
32
+ ) -> Tuple[DecisionId, Dict[str, Any]]:
33
+ decision_id = marker.marker_name.replace(MARKER_PREFIX, "")
34
+ props = json.decode(marker.details.data)
35
+ decision_type = DecisionType[props.pop("type")]
36
+ return DecisionId(decision_type, decision_id), props
@@ -0,0 +1,25 @@
1
+ from cadence._internal.workflow.statemachine.decision_state_machine import (
2
+ DecisionStateMachine,
3
+ DecisionId,
4
+ DecisionType,
5
+ )
6
+ from cadence.api.v1 import decision
7
+
8
+ COMPLETE = "complete"
9
+
10
+ COMPLETION_ID = DecisionId(DecisionType.WORKFLOW_COMPLETE, COMPLETE)
11
+
12
+
13
+ class CompletionStateMachine(DecisionStateMachine):
14
+ def __init__(self, outcome: decision.Decision) -> None:
15
+ super().__init__()
16
+ self.outcome = outcome
17
+
18
+ def get_id(self) -> DecisionId:
19
+ return COMPLETION_ID
20
+
21
+ def get_decision(self) -> decision.Decision | None:
22
+ return self.outcome
23
+
24
+ def request_cancel(self) -> bool:
25
+ return False