iwf-sdk 0.0.1__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 (199) hide show
  1. iwf/.openapi-python-client-config.yaml +1 -0
  2. iwf/__init__.py +0 -0
  3. iwf/client.py +554 -0
  4. iwf/client_options.py +20 -0
  5. iwf/command_request.py +116 -0
  6. iwf/command_results.py +102 -0
  7. iwf/communication.py +127 -0
  8. iwf/communication_schema.py +54 -0
  9. iwf/data_attributes.py +61 -0
  10. iwf/errors.py +118 -0
  11. iwf/iwf_api/__init__.py +8 -0
  12. iwf/iwf_api/api/__init__.py +1 -0
  13. iwf/iwf_api/api/default/__init__.py +1 -0
  14. iwf/iwf_api/api/default/get_info_healthcheck.py +122 -0
  15. iwf/iwf_api/api/default/post_api_v1_workflow_config_update.py +163 -0
  16. iwf/iwf_api/api/default/post_api_v1_workflow_dataobjects_get.py +165 -0
  17. iwf/iwf_api/api/default/post_api_v1_workflow_dataobjects_set.py +163 -0
  18. iwf/iwf_api/api/default/post_api_v1_workflow_get.py +165 -0
  19. iwf/iwf_api/api/default/post_api_v1_workflow_internal_dump.py +165 -0
  20. iwf/iwf_api/api/default/post_api_v1_workflow_reset.py +165 -0
  21. iwf/iwf_api/api/default/post_api_v1_workflow_rpc.py +165 -0
  22. iwf/iwf_api/api/default/post_api_v1_workflow_search.py +165 -0
  23. iwf/iwf_api/api/default/post_api_v1_workflow_searchattributes_get.py +165 -0
  24. iwf/iwf_api/api/default/post_api_v1_workflow_searchattributes_set.py +163 -0
  25. iwf/iwf_api/api/default/post_api_v1_workflow_signal.py +163 -0
  26. iwf/iwf_api/api/default/post_api_v1_workflow_start.py +165 -0
  27. iwf/iwf_api/api/default/post_api_v1_workflow_stop.py +163 -0
  28. iwf/iwf_api/api/default/post_api_v1_workflow_timer_skip.py +163 -0
  29. iwf/iwf_api/api/default/post_api_v_1_workflow_get_with_wait.py +165 -0
  30. iwf/iwf_api/api/default/post_api_v_1_workflow_state_decide.py +165 -0
  31. iwf/iwf_api/api/default/post_api_v_1_workflow_state_start.py +165 -0
  32. iwf/iwf_api/api/default/post_api_v_1_workflow_trigger_continue_as_new.py +163 -0
  33. iwf/iwf_api/api/default/post_api_v_1_workflow_wait_for_state_completion.py +161 -0
  34. iwf/iwf_api/api/default/post_api_v_1_workflow_worker_rpc.py +165 -0
  35. iwf/iwf_api/client.py +268 -0
  36. iwf/iwf_api/errors.py +16 -0
  37. iwf/iwf_api/models/__init__.py +163 -0
  38. iwf/iwf_api/models/channel_info.py +59 -0
  39. iwf/iwf_api/models/channel_request_status.py +9 -0
  40. iwf/iwf_api/models/command_combination.py +61 -0
  41. iwf/iwf_api/models/command_request.py +152 -0
  42. iwf/iwf_api/models/command_results.py +135 -0
  43. iwf/iwf_api/models/command_waiting_type.py +10 -0
  44. iwf/iwf_api/models/context.py +104 -0
  45. iwf/iwf_api/models/encoded_object.py +68 -0
  46. iwf/iwf_api/models/error_response.py +103 -0
  47. iwf/iwf_api/models/error_sub_status.py +12 -0
  48. iwf/iwf_api/models/execute_api_failure_policy.py +9 -0
  49. iwf/iwf_api/models/executing_state_id_mode.py +10 -0
  50. iwf/iwf_api/models/health_info.py +77 -0
  51. iwf/iwf_api/models/id_reuse_policy.py +11 -0
  52. iwf/iwf_api/models/inter_state_channel_command.py +88 -0
  53. iwf/iwf_api/models/inter_state_channel_publishing.py +83 -0
  54. iwf/iwf_api/models/inter_state_channel_result.py +100 -0
  55. iwf/iwf_api/models/key_value.py +81 -0
  56. iwf/iwf_api/models/persistence_loading_policy.py +98 -0
  57. iwf/iwf_api/models/persistence_loading_type.py +12 -0
  58. iwf/iwf_api/models/retry_policy.py +95 -0
  59. iwf/iwf_api/models/search_attribute.py +123 -0
  60. iwf/iwf_api/models/search_attribute_key_and_type.py +76 -0
  61. iwf/iwf_api/models/search_attribute_value_type.py +14 -0
  62. iwf/iwf_api/models/signal_command.py +88 -0
  63. iwf/iwf_api/models/signal_result.py +100 -0
  64. iwf/iwf_api/models/state_completion_output.py +91 -0
  65. iwf/iwf_api/models/state_decision.py +93 -0
  66. iwf/iwf_api/models/state_movement.py +110 -0
  67. iwf/iwf_api/models/timer_command.py +70 -0
  68. iwf/iwf_api/models/timer_result.py +69 -0
  69. iwf/iwf_api/models/timer_status.py +9 -0
  70. iwf/iwf_api/models/trigger_continue_as_new_request.py +70 -0
  71. iwf/iwf_api/models/wait_until_api_failure_policy.py +9 -0
  72. iwf/iwf_api/models/worker_error_response.py +68 -0
  73. iwf/iwf_api/models/workflow_already_started_options.py +70 -0
  74. iwf/iwf_api/models/workflow_conditional_close.py +98 -0
  75. iwf/iwf_api/models/workflow_conditional_close_type.py +10 -0
  76. iwf/iwf_api/models/workflow_config.py +103 -0
  77. iwf/iwf_api/models/workflow_config_executing_state_id_mode.py +10 -0
  78. iwf/iwf_api/models/workflow_config_update_request.py +84 -0
  79. iwf/iwf_api/models/workflow_dump_request.py +83 -0
  80. iwf/iwf_api/models/workflow_dump_response.py +75 -0
  81. iwf/iwf_api/models/workflow_error_type.py +13 -0
  82. iwf/iwf_api/models/workflow_get_data_objects_request.py +90 -0
  83. iwf/iwf_api/models/workflow_get_data_objects_response.py +75 -0
  84. iwf/iwf_api/models/workflow_get_request.py +88 -0
  85. iwf/iwf_api/models/workflow_get_response.py +121 -0
  86. iwf/iwf_api/models/workflow_get_search_attributes_request.py +95 -0
  87. iwf/iwf_api/models/workflow_get_search_attributes_response.py +75 -0
  88. iwf/iwf_api/models/workflow_reset_request.py +142 -0
  89. iwf/iwf_api/models/workflow_reset_response.py +59 -0
  90. iwf/iwf_api/models/workflow_reset_type.py +12 -0
  91. iwf/iwf_api/models/workflow_retry_policy.py +86 -0
  92. iwf/iwf_api/models/workflow_rpc_request.py +173 -0
  93. iwf/iwf_api/models/workflow_rpc_response.py +72 -0
  94. iwf/iwf_api/models/workflow_search_request.py +79 -0
  95. iwf/iwf_api/models/workflow_search_response.py +84 -0
  96. iwf/iwf_api/models/workflow_search_response_entry.py +67 -0
  97. iwf/iwf_api/models/workflow_set_data_objects_request.py +95 -0
  98. iwf/iwf_api/models/workflow_set_search_attributes_request.py +95 -0
  99. iwf/iwf_api/models/workflow_signal_request.py +100 -0
  100. iwf/iwf_api/models/workflow_skip_timer_request.py +96 -0
  101. iwf/iwf_api/models/workflow_start_options.py +196 -0
  102. iwf/iwf_api/models/workflow_start_request.py +174 -0
  103. iwf/iwf_api/models/workflow_start_response.py +59 -0
  104. iwf/iwf_api/models/workflow_state_execute_request.py +180 -0
  105. iwf/iwf_api/models/workflow_state_execute_response.py +184 -0
  106. iwf/iwf_api/models/workflow_state_options.py +284 -0
  107. iwf/iwf_api/models/workflow_state_wait_until_request.py +143 -0
  108. iwf/iwf_api/models/workflow_state_wait_until_response.py +184 -0
  109. iwf/iwf_api/models/workflow_status.py +14 -0
  110. iwf/iwf_api/models/workflow_stop_request.py +96 -0
  111. iwf/iwf_api/models/workflow_stop_type.py +10 -0
  112. iwf/iwf_api/models/workflow_wait_for_state_completion_request.py +97 -0
  113. iwf/iwf_api/models/workflow_wait_for_state_completion_response.py +72 -0
  114. iwf/iwf_api/models/workflow_worker_rpc_request.py +181 -0
  115. iwf/iwf_api/models/workflow_worker_rpc_request_internal_channel_infos.py +57 -0
  116. iwf/iwf_api/models/workflow_worker_rpc_request_signal_channel_infos.py +57 -0
  117. iwf/iwf_api/models/workflow_worker_rpc_response.py +193 -0
  118. iwf/iwf_api/py.typed +1 -0
  119. iwf/iwf_api/types.py +46 -0
  120. iwf/object_encoder.py +804 -0
  121. iwf/persistence.py +80 -0
  122. iwf/persistence_options.py +10 -0
  123. iwf/persistence_schema.py +44 -0
  124. iwf/py.typed +1 -0
  125. iwf/registry.py +195 -0
  126. iwf/reset_workflow_type_and_options.py +63 -0
  127. iwf/rpc.py +86 -0
  128. iwf/search_attributes.py +175 -0
  129. iwf/state_decision.py +147 -0
  130. iwf/state_execution_locals.py +57 -0
  131. iwf/state_movement.py +109 -0
  132. iwf/state_schema.py +40 -0
  133. iwf/stop_workflow_options.py +9 -0
  134. iwf/tests/__init__.py +70 -0
  135. iwf/tests/iwf-service-env/.env +7 -0
  136. iwf/tests/iwf-service-env/docker-compose-init.sh +44 -0
  137. iwf/tests/iwf-service-env/docker-compose.yml +85 -0
  138. iwf/tests/iwf-service-env/dynamicconfig/README.md +39 -0
  139. iwf/tests/iwf-service-env/dynamicconfig/development-sql.yaml +6 -0
  140. iwf/tests/iwf-service-env/dynamicconfig/docker.yaml +0 -0
  141. iwf/tests/test_abnormal_exit_workflow.py +34 -0
  142. iwf/tests/test_basic_workflow.py +61 -0
  143. iwf/tests/test_conditional_complete.py +42 -0
  144. iwf/tests/test_describe_workflow.py +31 -0
  145. iwf/tests/test_empty_data_decodes_properly.py +65 -0
  146. iwf/tests/test_internal_channel.py +19 -0
  147. iwf/tests/test_internal_channel_with_no_prefix_channel.py +32 -0
  148. iwf/tests/test_persistence_data_attributes.py +53 -0
  149. iwf/tests/test_persistence_search_attributes.py +118 -0
  150. iwf/tests/test_persistence_state_execution_locals.py +29 -0
  151. iwf/tests/test_rpc.py +55 -0
  152. iwf/tests/test_rpc_with_memo.py +186 -0
  153. iwf/tests/test_rpc_with_memo_duplicate_java_tests.py +108 -0
  154. iwf/tests/test_signal.py +42 -0
  155. iwf/tests/test_skip_wait_until.py +71 -0
  156. iwf/tests/test_state_failure_recovery.py +19 -0
  157. iwf/tests/test_timer.py +26 -0
  158. iwf/tests/test_wait_for_state_execution_completion.py +44 -0
  159. iwf/tests/test_workflow_errors.py +78 -0
  160. iwf/tests/test_workflow_state_options.py +109 -0
  161. iwf/tests/test_workflow_state_options_override.py +35 -0
  162. iwf/tests/worker_server.py +60 -0
  163. iwf/tests/workflows/abnormal_exit_workflow.py +34 -0
  164. iwf/tests/workflows/basic_workflow.py +55 -0
  165. iwf/tests/workflows/conditional_complete_workflow.py +87 -0
  166. iwf/tests/workflows/describe_workflow.py +38 -0
  167. iwf/tests/workflows/empty_data_workflow.py +37 -0
  168. iwf/tests/workflows/internal_channel_workflow.py +123 -0
  169. iwf/tests/workflows/internal_channel_workflow_with_no_prefix_channel.py +94 -0
  170. iwf/tests/workflows/java_duplicate_rpc_memo_workflow.py +271 -0
  171. iwf/tests/workflows/persistence_data_attributes_workflow.py +90 -0
  172. iwf/tests/workflows/persistence_search_attributes_workflow.py +153 -0
  173. iwf/tests/workflows/persistence_state_execution_local_workflow.py +55 -0
  174. iwf/tests/workflows/recovery_workflow.py +76 -0
  175. iwf/tests/workflows/rpc_memo_workflow.py +224 -0
  176. iwf/tests/workflows/rpc_workflow.py +111 -0
  177. iwf/tests/workflows/state_options_override_workflow.py +86 -0
  178. iwf/tests/workflows/state_options_workflow.py +78 -0
  179. iwf/tests/workflows/timer_workflow.py +38 -0
  180. iwf/tests/workflows/wait_for_state_with_state_execution_id_workflow.py +63 -0
  181. iwf/tests/workflows/wait_for_state_with_wait_for_key_workflow.py +64 -0
  182. iwf/tests/workflows/wait_internal_channel_workflow.py +39 -0
  183. iwf/tests/workflows/wait_signal_workflow.py +140 -0
  184. iwf/type_store.py +91 -0
  185. iwf/unregistered_client.py +581 -0
  186. iwf/utils/__init__.py +0 -0
  187. iwf/utils/iwf_typing.py +17 -0
  188. iwf/utils/persistence_utils.py +23 -0
  189. iwf/worker_service.py +427 -0
  190. iwf/workflow.py +72 -0
  191. iwf/workflow_context.py +36 -0
  192. iwf/workflow_info.py +7 -0
  193. iwf/workflow_options.py +65 -0
  194. iwf/workflow_state.py +120 -0
  195. iwf/workflow_state_options.py +146 -0
  196. iwf_sdk-0.0.1.dist-info/METADATA +143 -0
  197. iwf_sdk-0.0.1.dist-info/RECORD +199 -0
  198. iwf_sdk-0.0.1.dist-info/WHEEL +4 -0
  199. iwf_sdk-0.0.1.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1 @@
1
+ project_name_override: iwf_api
iwf/__init__.py ADDED
File without changes
iwf/client.py ADDED
@@ -0,0 +1,554 @@
1
+ import inspect
2
+ from typing import Any, Callable, List, Optional, Type, TypeVar, Union
3
+
4
+ from typing_extensions import deprecated
5
+
6
+ from iwf.client_options import ClientOptions
7
+ from iwf.errors import InvalidArgumentError, NotRegisteredError, WorkflowDefinitionError
8
+ from iwf.iwf_api.models import (
9
+ SearchAttribute,
10
+ SearchAttributeKeyAndType,
11
+ SearchAttributeValueType,
12
+ )
13
+ from iwf.iwf_api.types import Unset
14
+ from iwf.registry import Registry
15
+ from iwf.reset_workflow_type_and_options import ResetWorkflowTypeAndOptions
16
+ from iwf.stop_workflow_options import StopWorkflowOptions
17
+ from iwf.unregistered_client import UnregisteredClient, UnregisteredWorkflowOptions
18
+ from iwf.utils.iwf_typing import unset_to_none
19
+ from iwf.utils.persistence_utils import get_search_attribute_value
20
+ from iwf.workflow import ObjectWorkflow, get_workflow_type_by_class
21
+ from iwf.workflow_info import WorkflowInfo
22
+ from iwf.workflow_options import WorkflowOptions
23
+ from iwf.workflow_state import (
24
+ WorkflowState,
25
+ get_state_id,
26
+ get_state_id_by_class,
27
+ get_state_execution_id,
28
+ should_skip_wait_until,
29
+ )
30
+ from iwf.workflow_state_options import _to_idl_state_options
31
+
32
+ T = TypeVar("T")
33
+
34
+
35
+ def get_workflow_type_by_rpc_method(meth) -> str:
36
+ if inspect.ismethod(meth):
37
+ return inspect.getmro(meth.__self__.__class__)[0].__name__
38
+ if inspect.isfunction(meth):
39
+ return meth.__qualname__.split(".<locals>", 1)[0].rsplit(".", 1)[0]
40
+ raise InvalidArgumentError(f"method {meth} is not a RPC method")
41
+
42
+
43
+ class Client:
44
+ def __init__(self, registry: Registry, options: Optional[ClientOptions] = None):
45
+ self._registry = registry
46
+ if options is None:
47
+ options = ClientOptions.local_default()
48
+ self._options = options
49
+ self._unregistered_client = UnregisteredClient(options)
50
+
51
+ def start_workflow(
52
+ self,
53
+ wf_class: type[ObjectWorkflow],
54
+ wf_id: str,
55
+ timeout_seconds: int,
56
+ input: Any = None,
57
+ options: Optional[WorkflowOptions] = None,
58
+ ) -> str:
59
+ """
60
+
61
+ Args:
62
+ wf_class: the workflow definition class
63
+ wf_id: workflowId
64
+ timeout_seconds: the timeout. Use zero for infinite timeout(only works for Temporal as backend)
65
+ input: input of the workflow, aka, the input of the starting state of the workflow
66
+ options: advanced options
67
+
68
+ Returns:
69
+ workflow_run_id: the run id of the started workflow
70
+
71
+ Raises:
72
+ ClientSideError for non-retryable error
73
+ ServerSideError for server error
74
+ """
75
+ wf_type = get_workflow_type_by_class(wf_class)
76
+ self._registry.get_workflow_with_check(wf_type)
77
+
78
+ starting_state = self._registry.get_workflow_starting_state(wf_type)
79
+ unreg_opts = UnregisteredWorkflowOptions()
80
+
81
+ if options is not None:
82
+ unreg_opts.workflow_id_reuse_policy = options.workflow_id_reuse_policy
83
+ unreg_opts.workflow_retry_policy = options.workflow_retry_policy
84
+ unreg_opts.cron_schedule = options.workflow_cron_schedule
85
+ unreg_opts.workflow_start_delay_seconds = (
86
+ options.workflow_start_delay_seconds
87
+ )
88
+ unreg_opts.workflow_already_started_options = (
89
+ options.workflow_already_started_options
90
+ )
91
+ unreg_opts.initial_data_attributes = options.initial_data_attributes
92
+
93
+ unreg_opts.workflow_config_override = options.workflow_config_override
94
+
95
+ unreg_opts.wait_for_completion_state_ids = (
96
+ options.wait_for_completion_state_ids
97
+ )
98
+ unreg_opts.wait_for_completion_state_execution_ids = (
99
+ options.wait_for_completion_state_execution_ids
100
+ )
101
+
102
+ if options.initial_search_attributes:
103
+ sa_types = self._registry.get_search_attribute_types(wf_type)
104
+ converted_sas = convert_to_sa_list(
105
+ sa_types, options.initial_search_attributes
106
+ )
107
+ unreg_opts.initial_search_attributes = converted_sas
108
+
109
+ schema_options = self._registry.get_persistence_options(wf_type)
110
+ if schema_options.enable_caching:
111
+ unreg_opts.using_memo_for_data_attributes = schema_options.enable_caching
112
+
113
+ starting_state_id = None
114
+
115
+ if starting_state is not None:
116
+ starting_state_id = get_state_id(starting_state)
117
+ starting_state_opts = _to_idl_state_options(
118
+ should_skip_wait_until(starting_state),
119
+ starting_state.get_state_options(),
120
+ self._registry.get_state_store(wf_type),
121
+ )
122
+ unreg_opts.start_state_options = starting_state_opts
123
+
124
+ return self._unregistered_client.start_workflow(
125
+ wf_type, wf_id, starting_state_id, timeout_seconds, input, unreg_opts
126
+ )
127
+
128
+ @deprecated("use wait_for_workflow_completion instead")
129
+ def get_simple_workflow_result_with_wait(
130
+ self,
131
+ workflow_id: str,
132
+ type_hint: Optional[Type[T]] = None,
133
+ ) -> Optional[T]:
134
+ return self._unregistered_client.get_simple_workflow_result_with_wait(
135
+ workflow_id, "", type_hint
136
+ )
137
+
138
+ def wait_for_workflow_completion(
139
+ self,
140
+ workflow_id: str,
141
+ type_hint: Optional[Type[T]] = None,
142
+ ) -> Optional[T]:
143
+ """
144
+ This will be waiting up to 5~60 seconds (configurable in HTTP client and capped by server) for workflow to
145
+ complete, and return the workflow completion result.
146
+ Args:
147
+ workflow_id: workflowId
148
+ type_hint: the type of workflow result
149
+
150
+ Returns:
151
+ the completion result if there is one
152
+ Raises
153
+ WorkflowAbnormalExitError if workflow failed/timeout/canceled/terminated
154
+ WorkflowStillRunningError if workflow is still running after exceeding the waiting timeout(HTTP timeout)
155
+ ClientSideError for non-retryable error
156
+ ServerSideError for server error
157
+ """
158
+ return self._unregistered_client.get_simple_workflow_result_with_wait(
159
+ workflow_id, "", type_hint
160
+ )
161
+
162
+ def stop_workflow(
163
+ self,
164
+ workflow_id: str,
165
+ options: Optional[StopWorkflowOptions] = None,
166
+ ):
167
+ return self._unregistered_client.stop_workflow(workflow_id, "", options)
168
+
169
+ def get_all_workflow_data_attributes(
170
+ self,
171
+ workflow_class: type[ObjectWorkflow],
172
+ workflow_id: str,
173
+ workflow_run_id: str = "",
174
+ ):
175
+ return self.get_workflow_data_attributes(
176
+ workflow_class, workflow_id, workflow_run_id, None
177
+ )
178
+
179
+ def get_workflow_data_attributes(
180
+ self,
181
+ workflow_class: type[ObjectWorkflow],
182
+ workflow_id: str,
183
+ workflow_run_id: str = "",
184
+ keys: Optional[List[str]] = None,
185
+ ):
186
+ wf_type = get_workflow_type_by_class(workflow_class)
187
+ data_attr_type_store = self._registry.get_data_attribute_types(wf_type)
188
+ if keys:
189
+ for key in keys:
190
+ if not data_attr_type_store.is_valid_name_or_prefix(key):
191
+ raise NotRegisteredError(
192
+ f"key {key} is not registered in workflow {wf_type}"
193
+ )
194
+
195
+ schema_options = self._registry.get_persistence_options(wf_type)
196
+
197
+ response = self._unregistered_client.get_workflow_data_attributes(
198
+ workflow_id, workflow_run_id, keys, schema_options.enable_caching
199
+ )
200
+
201
+ if not response.objects:
202
+ raise RuntimeError("data attributes not returned")
203
+
204
+ res = {}
205
+ for kv in response.objects:
206
+ k = unset_to_none(kv.key)
207
+ if k and kv.value:
208
+ res[kv.key] = self._options.object_encoder.decode(
209
+ kv.value, data_attr_type_store.get_type(k)
210
+ )
211
+
212
+ return res
213
+
214
+ def set_workflow_data_attributes(
215
+ self,
216
+ workflow_class: type[ObjectWorkflow],
217
+ workflow_id: str,
218
+ workflow_run_id: str = "",
219
+ data_attributes: dict[str, Any] = dict(),
220
+ ):
221
+ wf_type = get_workflow_type_by_class(workflow_class)
222
+ data_attr_type_store = self._registry.get_data_attribute_types(wf_type)
223
+ for key, value in data_attributes.items():
224
+ if not data_attr_type_store.is_valid_name_or_prefix(key):
225
+ raise NotRegisteredError(f"data attribute {key} is not registered")
226
+
227
+ data_attr_type = data_attr_type_store.get_type(key)
228
+ if not isinstance(value, data_attr_type):
229
+ raise NotRegisteredError(
230
+ f"data attribute {key} is not registered as {type(value)}"
231
+ )
232
+ return self._unregistered_client.set_workflow_data_attributes(
233
+ workflow_id, workflow_run_id, data_attributes
234
+ )
235
+
236
+ def invoke_rpc(
237
+ self,
238
+ workflow_id: str,
239
+ rpc: Callable, # this can be a function: RPCWorkflow.rpc_method or a method: workflow_instance.rpc_method
240
+ input: Any = None,
241
+ return_type_hint: Optional[Type[T]] = None,
242
+ ) -> Optional[T]:
243
+ wf_type = get_workflow_type_by_rpc_method(rpc)
244
+ rpc_name = rpc.__name__
245
+ rpc_info = self._registry.get_rpc_infos(wf_type)[rpc_name]
246
+ schema_options = self._registry.get_persistence_options(wf_type)
247
+ use_memo = schema_options.enable_caching
248
+ if rpc_info.bypass_caching_for_strong_consistency:
249
+ use_memo = False
250
+
251
+ return self._unregistered_client.invoke_rpc(
252
+ input=input,
253
+ workflow_id=workflow_id,
254
+ workflow_run_id="",
255
+ rpc_name=rpc_name,
256
+ timeout_seconds=rpc_info.timeout_seconds,
257
+ data_attribute_policy=rpc_info.data_attribute_loading_policy,
258
+ use_memo_for_data_attributes=use_memo,
259
+ all_defined_search_attribute_types=[],
260
+ return_type_hint=return_type_hint,
261
+ )
262
+
263
+ def signal_workflow(
264
+ self,
265
+ workflow_id: str,
266
+ signal_channel_name: str,
267
+ signal_value: Optional[Any] = None,
268
+ ):
269
+ return self._unregistered_client.signal_workflow(
270
+ workflow_id, "", signal_channel_name, signal_value
271
+ )
272
+
273
+ def reset_workflow(
274
+ self,
275
+ workflow_id: str,
276
+ reset_workflow_type_and_options: ResetWorkflowTypeAndOptions,
277
+ ):
278
+ return self._unregistered_client.reset_workflow(
279
+ workflow_id, "", reset_workflow_type_and_options
280
+ )
281
+
282
+ def describe_workflow(
283
+ self,
284
+ workflow_id: str,
285
+ workflow_run_id: Optional[str] = None,
286
+ ):
287
+ run_id = workflow_run_id if workflow_run_id is not None else ""
288
+
289
+ response = self._unregistered_client.get_workflow(workflow_id, run_id)
290
+ return WorkflowInfo(workflow_status=response.workflow_status)
291
+
292
+ def skip_timer_by_command_id(
293
+ self,
294
+ workflow_id: str,
295
+ workflow_state_id: str,
296
+ timer_command_id: str,
297
+ state_execution_number: int = 1,
298
+ ):
299
+ return self._unregistered_client.skip_timer_by_command_id(
300
+ workflow_id,
301
+ "",
302
+ workflow_state_id,
303
+ timer_command_id=timer_command_id,
304
+ state_execution_number=state_execution_number,
305
+ )
306
+
307
+ def skip_timer_at_command_index(
308
+ self,
309
+ workflow_id: str,
310
+ workflow_state_id: Union[str, type[WorkflowState]],
311
+ state_execution_number: int = 1,
312
+ timer_command_index: int = 0,
313
+ ):
314
+ if isinstance(workflow_state_id, type):
315
+ state_id = get_state_id_by_class(workflow_state_id)
316
+ else:
317
+ state_id = workflow_state_id
318
+ return self._unregistered_client.skip_timer_at_command_index(
319
+ workflow_id,
320
+ "",
321
+ state_id,
322
+ state_execution_number,
323
+ timer_command_index,
324
+ )
325
+
326
+ def get_all_search_attributes(
327
+ self,
328
+ workflow_class: type[ObjectWorkflow],
329
+ workflow_id: str,
330
+ workflow_run_id: Optional[str] = None,
331
+ ):
332
+ run_id = workflow_run_id if workflow_run_id is not None else ""
333
+
334
+ return self._do_get_workflow_search_attributes(
335
+ workflow_class, workflow_id, run_id
336
+ )
337
+
338
+ def get_workflow_search_attributes(
339
+ self,
340
+ workflow_class: type[ObjectWorkflow],
341
+ workflow_id: str,
342
+ attribute_keys: list[str],
343
+ workflow_run_id: Optional[str] = None,
344
+ ):
345
+ if not attribute_keys:
346
+ raise ValueError(
347
+ "attribute_keys must contain at least one entry, or use get_all_search_attributes API to get all"
348
+ )
349
+
350
+ run_id = workflow_run_id if workflow_run_id is not None else ""
351
+
352
+ return self._do_get_workflow_search_attributes(
353
+ workflow_class, workflow_id, run_id, attribute_keys
354
+ )
355
+
356
+ def _do_get_workflow_search_attributes(
357
+ self,
358
+ workflow_class: type[ObjectWorkflow],
359
+ workflow_id: str,
360
+ workflow_run_id: str,
361
+ attribute_keys: Optional[list[str]] = None,
362
+ ):
363
+ wf_type = get_workflow_type_by_class(workflow_class)
364
+ self._registry.get_workflow_with_check(wf_type)
365
+
366
+ search_attribute_types = self._registry.get_search_attribute_types(wf_type)
367
+
368
+ # if attribute keys is None, will fetch all registered search attributes from the server
369
+ if attribute_keys:
370
+ non_existing_search_attribute_list: list[str] = []
371
+ for attribute_key in attribute_keys:
372
+ if attribute_key not in search_attribute_types:
373
+ non_existing_search_attribute_list.append(attribute_key)
374
+
375
+ if non_existing_search_attribute_list:
376
+ raise InvalidArgumentError(
377
+ f"Search attributes not registered: {','.join(non_existing_search_attribute_list)}"
378
+ )
379
+
380
+ key_and_types: list[SearchAttributeKeyAndType] = []
381
+ if attribute_keys is None:
382
+ for attribute_key, sa_type in search_attribute_types.items():
383
+ key_and_types.append(SearchAttributeKeyAndType(attribute_key, sa_type))
384
+ else:
385
+ for attribute_key in attribute_keys:
386
+ sa_type = search_attribute_types[attribute_key]
387
+ key_and_types.append(SearchAttributeKeyAndType(attribute_key, sa_type))
388
+
389
+ response = self._unregistered_client.get_workflow_search_attributes(
390
+ workflow_id, workflow_run_id, key_and_types
391
+ )
392
+
393
+ response_sas = response.search_attributes
394
+
395
+ # TODO: troubleshoot why unset_to_none doesn't work as expected with lists
396
+ if isinstance(response_sas, Unset) or response_sas is None:
397
+ raise RuntimeError("search attributes not returned")
398
+
399
+ result: dict[str, Any] = {}
400
+
401
+ for response_sa in response_sas:
402
+ response_sa_key = unset_to_none(response_sa.key)
403
+ if response_sa_key is None:
404
+ raise RuntimeError("search attribute key is None")
405
+ response_sa_type = search_attribute_types[response_sa_key]
406
+ value = get_search_attribute_value(response_sa_type, response_sa)
407
+ result[response_sa_key] = value
408
+
409
+ return result
410
+
411
+ def set_workflow_search_attributes(
412
+ self,
413
+ workflow_class: type[ObjectWorkflow],
414
+ workflow_id: str,
415
+ search_attributes: list[SearchAttribute],
416
+ workflow_run_id: Optional[str] = None,
417
+ ):
418
+ run_id = workflow_run_id if workflow_run_id is not None else ""
419
+
420
+ return self._do_set_workflow_search_attributes(
421
+ workflow_class, workflow_id, run_id, search_attributes
422
+ )
423
+
424
+ """A long poll API to wait for the completion of the state.
425
+ Note 1 The state_completion to wait for is needed to registered on starting workflow due to limitation in https://github.com/superdurable/iwf/issues/349
426
+ Note 2 The max polling time is configured in client_options (default to 10s)
427
+
428
+ Args:
429
+ state_class the state class.
430
+ workflow_id the workflowId
431
+ state_execution_number the state execution number. E.g. if it's 2, it means the 2nd execution of the state
432
+ """
433
+
434
+ def wait_for_state_execution_completion_with_state_execution_id(
435
+ self,
436
+ state_class: type[WorkflowState],
437
+ workflow_id: str,
438
+ state_execution_number: int = 1,
439
+ ):
440
+ state_execution_id = get_state_execution_id(state_class, state_execution_number)
441
+
442
+ self._unregistered_client.wait_for_state_execution_completion_with_state_execution_id(
443
+ workflow_id, state_execution_id
444
+ )
445
+
446
+ """A long poll API to wait for the completion of the state.
447
+ Note 1 The state_completion to wait for is needed to registered on starting workflow due to limitation in https://github.com/superdurable/iwf/issues/349
448
+ Note 2 The max polling time is configured in client_options (default to 10s)
449
+
450
+ Args:
451
+ state_class the state class.
452
+ workflow_id the workflowId
453
+ wait_for_key key provided by the client and to identity workflow
454
+ """
455
+
456
+ def wait_for_state_execution_completion_with_wait_for_key(
457
+ self, state_class: type[WorkflowState], workflow_id: str, wait_for_key: str
458
+ ):
459
+ state_id = get_state_id_by_class(state_class)
460
+
461
+ self._unregistered_client.wait_for_state_execution_completion_with_wait_for_key(
462
+ workflow_id, state_id, wait_for_key
463
+ )
464
+
465
+ def _do_set_workflow_search_attributes(
466
+ self,
467
+ workflow_class: type[ObjectWorkflow],
468
+ workflow_id: str,
469
+ workflow_run_id: str,
470
+ search_attributes: list[SearchAttribute],
471
+ ):
472
+ wf_type = get_workflow_type_by_class(workflow_class)
473
+ self._registry.get_workflow_with_check(wf_type)
474
+
475
+ search_attribute_types = self._registry.get_search_attribute_types(wf_type)
476
+
477
+ # Check that the requested sa type is registered to the key
478
+ for search_attribute in search_attributes:
479
+ sa_key = unset_to_none(search_attribute.key)
480
+ if sa_key is None:
481
+ raise RuntimeError("search attribute key is None")
482
+ if sa_key not in search_attribute_types:
483
+ raise InvalidArgumentError(f"Search attribute not registered: {sa_key}")
484
+ registered_value_type = search_attribute_types[sa_key]
485
+
486
+ sa_value_type = unset_to_none(search_attribute.value_type)
487
+ if sa_value_type is None:
488
+ raise RuntimeError("search value type is None")
489
+
490
+ if (
491
+ sa_value_type is not None
492
+ and registered_value_type != sa_value_type.value
493
+ ):
494
+ raise ValueError(
495
+ f"Search attribute key, {sa_key} is registered to type {registered_value_type}, but tried to add search attribute type {sa_value_type.value}"
496
+ )
497
+
498
+ self._unregistered_client.set_workflow_search_attributes(
499
+ workflow_id, workflow_run_id, search_attributes
500
+ )
501
+
502
+
503
+ def convert_to_sa_list(
504
+ sa_types: dict[str, SearchAttributeValueType], initial_sas: dict[str, Any]
505
+ ):
506
+ converted_sas: list[SearchAttribute] = []
507
+ if initial_sas:
508
+ for initial_sa_key, initial_sa_val in initial_sas.items():
509
+ if initial_sa_key not in sa_types:
510
+ raise WorkflowDefinitionError(
511
+ f"key {initial_sa_key} is not defined as search attribute, all keys are: {','.join(sa_types)}"
512
+ )
513
+
514
+ val_type = sa_types[initial_sa_key]
515
+ new_sa = SearchAttribute(key=initial_sa_key, value_type=val_type)
516
+ is_val_correct_type = False
517
+ if val_type == SearchAttributeValueType.INT:
518
+ if isinstance(initial_sa_val, int):
519
+ new_sa.integer_value = initial_sa_val
520
+ converted_sas.append(new_sa)
521
+ is_val_correct_type = True
522
+ elif val_type == SearchAttributeValueType.DOUBLE:
523
+ if isinstance(initial_sa_val, float):
524
+ new_sa.double_value = initial_sa_val
525
+ converted_sas.append(new_sa)
526
+ is_val_correct_type = True
527
+ elif val_type == SearchAttributeValueType.BOOL:
528
+ if isinstance(initial_sa_val, bool):
529
+ new_sa.bool_value = initial_sa_val
530
+ converted_sas.append(new_sa)
531
+ is_val_correct_type = True
532
+ elif (
533
+ val_type == SearchAttributeValueType.KEYWORD
534
+ or val_type == SearchAttributeValueType.TEXT
535
+ or val_type == SearchAttributeValueType.DATETIME
536
+ ):
537
+ if isinstance(initial_sa_val, str):
538
+ new_sa.string_value = initial_sa_val
539
+ converted_sas.append(new_sa)
540
+ is_val_correct_type = True
541
+ elif val_type == SearchAttributeValueType.KEYWORD_ARRAY:
542
+ if isinstance(initial_sa_val, list):
543
+ new_sa.string_array_value = initial_sa_val
544
+ converted_sas.append(new_sa)
545
+ is_val_correct_type = True
546
+ else:
547
+ raise ValueError("unsupported type")
548
+
549
+ if not is_val_correct_type:
550
+ raise InvalidArgumentError(
551
+ f"search attribute value is not set correctly for key {initial_sa_key} with value type {val_type}"
552
+ )
553
+
554
+ return converted_sas
iwf/client_options.py ADDED
@@ -0,0 +1,20 @@
1
+ from dataclasses import dataclass
2
+
3
+ from iwf.object_encoder import ObjectEncoder
4
+
5
+
6
+ @dataclass
7
+ class ClientOptions:
8
+ server_url: str
9
+ worker_url: str
10
+ object_encoder: ObjectEncoder
11
+ api_timeout: int = 60
12
+ long_poll_api_max_wait_time_seconds: int = 10
13
+
14
+ @classmethod
15
+ def local_default(cls):
16
+ return ClientOptions(
17
+ server_url="http://localhost:8801",
18
+ worker_url="http://localhost:8802",
19
+ object_encoder=ObjectEncoder.default,
20
+ )
iwf/command_request.py ADDED
@@ -0,0 +1,116 @@
1
+ from dataclasses import dataclass
2
+ from typing import Optional, Union
3
+
4
+ from iwf.errors import WorkflowDefinitionError
5
+ from iwf.iwf_api.models import CommandWaitingType
6
+ from iwf.iwf_api.models.command_combination import CommandCombination
7
+ from iwf.iwf_api.models.command_request import (
8
+ CommandRequest as IdlCommandRequest,
9
+ )
10
+ from iwf.iwf_api.models.inter_state_channel_command import (
11
+ InterStateChannelCommand as IdlInternalChannelCommand,
12
+ )
13
+ from iwf.iwf_api.models.signal_command import SignalCommand as IdlSignalCommand
14
+ from iwf.iwf_api.models.timer_command import TimerCommand as IdlTimerCommand
15
+
16
+
17
+ @dataclass
18
+ class TimerCommand:
19
+ command_id: str
20
+ duration_seconds: int
21
+
22
+ @classmethod
23
+ def by_seconds(cls, duration_seconds: int, command_id: Optional[str] = None):
24
+ return TimerCommand(
25
+ command_id if command_id is not None else "", duration_seconds
26
+ )
27
+
28
+
29
+ @dataclass
30
+ class InternalChannelCommand:
31
+ command_id: str
32
+ channel_name: str
33
+
34
+ @classmethod
35
+ def by_name(cls, channel_name: str, command_id: Optional[str] = None):
36
+ return InternalChannelCommand(
37
+ command_id if command_id is not None else "", channel_name
38
+ )
39
+
40
+
41
+ @dataclass
42
+ class SignalChannelCommand:
43
+ command_id: str
44
+ channel_name: str
45
+
46
+ @classmethod
47
+ def by_name(cls, channel_name: str, command_id: Optional[str] = None):
48
+ return SignalChannelCommand(
49
+ command_id if command_id is not None else "",
50
+ channel_name,
51
+ )
52
+
53
+
54
+ BaseCommand = Union[TimerCommand, InternalChannelCommand, SignalChannelCommand]
55
+
56
+
57
+ @dataclass
58
+ class CommandRequest:
59
+ commands: list[BaseCommand]
60
+ command_waiting_type: CommandWaitingType
61
+ command_combinations: list[CommandCombination]
62
+
63
+ @classmethod
64
+ def for_any_command_completed(cls, *commands: BaseCommand):
65
+ bc = [c for c in commands]
66
+ return CommandRequest(bc, CommandWaitingType.ANY_COMPLETED, [])
67
+
68
+ @classmethod
69
+ def for_all_command_completed(cls, *commands: BaseCommand):
70
+ bc = [c for c in commands]
71
+ return CommandRequest(bc, CommandWaitingType.ALL_COMPLETED, [])
72
+
73
+ @classmethod
74
+ def for_any_command_combination_completed(
75
+ cls, command_combinations_list: list[list[str]], *commands: BaseCommand
76
+ ):
77
+ return CommandRequest(
78
+ list(commands),
79
+ CommandWaitingType.ANY_COMBINATION_COMPLETED,
80
+ [CommandCombination(c) for c in command_combinations_list],
81
+ )
82
+
83
+ @classmethod
84
+ def empty(cls):
85
+ return CommandRequest(list(), CommandWaitingType.ALL_COMPLETED, [])
86
+
87
+
88
+ def _to_idl_command_request(request: CommandRequest) -> IdlCommandRequest:
89
+ req = IdlCommandRequest(
90
+ command_waiting_type=request.command_waiting_type,
91
+ )
92
+
93
+ timer_commands = []
94
+ internal_channel_commands = []
95
+ signal_commands = []
96
+ for t in request.commands:
97
+ if isinstance(t, TimerCommand):
98
+ timer_commands.append(IdlTimerCommand(t.duration_seconds, t.command_id))
99
+ elif isinstance(t, InternalChannelCommand):
100
+ internal_channel_commands.append(
101
+ IdlInternalChannelCommand(t.channel_name, t.command_id)
102
+ )
103
+ elif isinstance(t, SignalChannelCommand):
104
+ signal_commands.append(IdlSignalCommand(t.channel_name, t.command_id))
105
+ else:
106
+ raise WorkflowDefinitionError(f"unknown command {t.__class__.__qualname__}")
107
+
108
+ if len(timer_commands) > 0:
109
+ req.timer_commands = timer_commands
110
+ if len(internal_channel_commands) > 0:
111
+ req.inter_state_channel_commands = internal_channel_commands
112
+ if len(signal_commands) > 0:
113
+ req.signal_commands = signal_commands
114
+ if len(request.command_combinations) > 0:
115
+ req.command_combinations = request.command_combinations
116
+ return req