|
2 | 2 | from contextlib import contextmanager |
3 | 3 | from contextvars import ContextVar |
4 | 4 | from dataclasses import dataclass |
5 | | -from functools import update_wrapper |
6 | | -from inspect import signature, Parameter |
7 | | -from typing import Iterator, Callable, TypeVar, ParamSpec, Generic, TypedDict, Unpack, overload, get_type_hints, Type, Any |
| 5 | +from typing import Iterator, Callable, TypeVar, TypedDict, Type |
| 6 | +from functools import wraps |
8 | 7 |
|
9 | 8 | from cadence.client import Client |
10 | 9 |
|
11 | | - |
12 | | -@dataclass(frozen=True) |
13 | | -class WorkflowParameter: |
14 | | - """Parameter information for a workflow function.""" |
15 | | - name: str |
16 | | - type_hint: Type | None |
17 | | - default_value: Any | None |
| 10 | +T = TypeVar('T') |
18 | 11 |
|
19 | 12 |
|
20 | 13 | class WorkflowDefinitionOptions(TypedDict, total=False): |
21 | 14 | """Options for defining a workflow.""" |
22 | 15 | name: str |
23 | 16 |
|
24 | 17 |
|
25 | | -P = ParamSpec('P') |
26 | | -T = TypeVar('T') |
27 | | - |
28 | | - |
29 | | -class WorkflowDefinition(Generic[P, T]): |
| 18 | +class WorkflowDefinition: |
30 | 19 | """ |
31 | | - Definition of a workflow function with metadata. |
| 20 | + Definition of a workflow class with metadata. |
32 | 21 |
|
33 | | - Similar to ActivityDefinition but for workflows. |
34 | | - Provides type safety and metadata for workflow functions. |
| 22 | + Similar to ActivityDefinition but for workflow classes. |
| 23 | + Provides type safety and metadata for workflow classes. |
35 | 24 | """ |
36 | 25 |
|
37 | | - def __init__(self, wrapped: Callable[P, T], name: str, params: list[WorkflowParameter]): |
38 | | - self._wrapped = wrapped |
| 26 | + def __init__(self, cls: Type, name: str): |
| 27 | + self._cls = cls |
39 | 28 | self._name = name |
40 | | - self._params = params |
41 | | - update_wrapper(self, wrapped) |
42 | | - |
43 | | - def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T: |
44 | | - return self._wrapped(*args, **kwargs) |
45 | 29 |
|
46 | 30 | @property |
47 | 31 | def name(self) -> str: |
48 | 32 | """Get the workflow name.""" |
49 | 33 | return self._name |
50 | 34 |
|
51 | 35 | @property |
52 | | - def params(self) -> list[WorkflowParameter]: |
53 | | - """Get the workflow parameters.""" |
54 | | - return self._params |
55 | | - |
56 | | - @property |
57 | | - def fn(self) -> Callable[P, T]: |
58 | | - """Get the underlying workflow function.""" |
59 | | - return self._wrapped |
| 36 | + def cls(self) -> Type: |
| 37 | + """Get the workflow class.""" |
| 38 | + return self._cls |
60 | 39 |
|
61 | | - @classmethod |
62 | | - def wrap(cls, fn: Callable[P, T], opts: WorkflowDefinitionOptions) -> 'WorkflowDefinition[P, T]': |
| 40 | + @staticmethod |
| 41 | + def wrap(cls: Type, opts: WorkflowDefinitionOptions) -> 'WorkflowDefinition': |
63 | 42 | """ |
64 | | - Wrap a function as a WorkflowDefinition. |
| 43 | + Wrap a class as a WorkflowDefinition. |
65 | 44 |
|
66 | 45 | Args: |
67 | | - fn: The workflow function to wrap |
| 46 | + cls: The workflow class to wrap |
68 | 47 | opts: Options for the workflow definition |
69 | 48 |
|
70 | 49 | Returns: |
71 | 50 | A WorkflowDefinition instance |
| 51 | +
|
| 52 | + Raises: |
| 53 | + ValueError: If no run method is found or multiple run methods exist |
72 | 54 | """ |
73 | | - name = fn.__qualname__ |
| 55 | + name = cls.__name__ |
74 | 56 | if "name" in opts and opts["name"]: |
75 | 57 | name = opts["name"] |
76 | 58 |
|
77 | | - params = _get_workflow_params(fn) |
78 | | - return cls(fn, name, params) |
| 59 | + # Validate that the class has exactly one run method |
| 60 | + run_method_count = 0 |
| 61 | + for attr_name in dir(cls): |
| 62 | + if attr_name.startswith('_'): |
| 63 | + continue |
79 | 64 |
|
| 65 | + attr = getattr(cls, attr_name) |
| 66 | + if not callable(attr): |
| 67 | + continue |
80 | 68 |
|
81 | | -WorkflowDecorator = Callable[[Callable[P, T]], WorkflowDefinition[P, T]] |
| 69 | + # Check for workflow run method |
| 70 | + if hasattr(attr, '_workflow_run'): |
| 71 | + run_method_count += 1 |
82 | 72 |
|
| 73 | + if run_method_count == 0: |
| 74 | + raise ValueError(f"No @workflow.run method found in class {cls.__name__}") |
| 75 | + elif run_method_count > 1: |
| 76 | + raise ValueError(f"Multiple @workflow.run methods found in class {cls.__name__}") |
83 | 77 |
|
84 | | -@overload |
85 | | -def defn(fn: Callable[P, T]) -> WorkflowDefinition[P, T]: |
86 | | - ... |
| 78 | + return WorkflowDefinition(cls, name) |
87 | 79 |
|
88 | 80 |
|
89 | | -@overload |
90 | | -def defn(**kwargs: Unpack[WorkflowDefinitionOptions]) -> WorkflowDecorator: |
91 | | - ... |
| 81 | +def run(func: Callable[..., T]) -> Callable[..., T]: |
| 82 | + """ |
| 83 | + Decorator to mark a method as the main workflow run method. |
92 | 84 |
|
| 85 | + Args: |
| 86 | + func: The method to mark as the workflow run method |
93 | 87 |
|
94 | | -def defn(fn: Callable[P, T] | None = None, **kwargs: Unpack[WorkflowDefinitionOptions]) -> WorkflowDecorator | WorkflowDefinition[P, T]: |
| 88 | + Returns: |
| 89 | + The decorated method with workflow run metadata |
95 | 90 | """ |
96 | | - Decorator to define a workflow function. |
| 91 | + @wraps(func) |
| 92 | + def wrapper(*args, **kwargs): |
| 93 | + return func(*args, **kwargs) |
97 | 94 |
|
98 | | - Usage: |
99 | | - @defn |
100 | | - def my_workflow(input_data: str) -> str: |
101 | | - return f"processed: {input_data}" |
| 95 | + # Attach metadata to the function |
| 96 | + wrapper._workflow_run = True # type: ignore |
| 97 | + return wrapper |
102 | 98 |
|
103 | | - @defn(name="custom_workflow_name") |
104 | | - def my_other_workflow(input_data: str) -> str: |
105 | | - return f"custom: {input_data}" |
106 | 99 |
|
107 | | - Args: |
108 | | - fn: The workflow function (when used without parentheses) |
109 | | - **kwargs: Workflow definition options |
| 100 | +# Create a simple namespace object for the workflow decorators |
| 101 | +class _WorkflowNamespace: |
| 102 | + run = staticmethod(run) |
110 | 103 |
|
111 | | - Returns: |
112 | | - Either a WorkflowDefinition (direct decoration) or a decorator function |
113 | | - """ |
114 | | - opts = WorkflowDefinitionOptions(**kwargs) |
115 | | - |
116 | | - def decorator(inner_fn: Callable[P, T]) -> WorkflowDefinition[P, T]: |
117 | | - return WorkflowDefinition.wrap(inner_fn, opts) |
118 | | - |
119 | | - if fn is not None: |
120 | | - return decorator(fn) |
121 | | - |
122 | | - return decorator |
123 | | - |
124 | | - |
125 | | -def _get_workflow_params(fn: Callable) -> list[WorkflowParameter]: |
126 | | - """Extract parameter information from a workflow function.""" |
127 | | - args = signature(fn).parameters |
128 | | - hints = get_type_hints(fn) |
129 | | - result = [] |
130 | | - for name, param in args.items(): |
131 | | - # Filter out self parameter |
132 | | - if param.name == "self": |
133 | | - continue |
134 | | - default = None |
135 | | - if param.default != Parameter.empty: |
136 | | - default = param.default |
137 | | - if param.kind in (Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD): |
138 | | - type_hint = hints.get(name, None) |
139 | | - result.append(WorkflowParameter(name, type_hint, default)) |
140 | | - else: |
141 | | - raise ValueError(f"Parameters must be positional. {name} is {param.kind}, and not valid") |
142 | | - |
143 | | - return result |
| 104 | +workflow = _WorkflowNamespace() |
144 | 105 |
|
145 | 106 |
|
146 | 107 | @dataclass |
|
0 commit comments