azure-functions-durable 1.2.8__py3-none-any.whl → 1.2.10__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.
- azure/durable_functions/__init__.py +81 -81
- azure/durable_functions/constants.py +9 -9
- azure/durable_functions/decorators/__init__.py +3 -3
- azure/durable_functions/decorators/durable_app.py +249 -249
- azure/durable_functions/decorators/metadata.py +109 -109
- azure/durable_functions/entity.py +125 -125
- azure/durable_functions/models/DurableEntityContext.py +201 -201
- azure/durable_functions/models/DurableHttpRequest.py +58 -58
- azure/durable_functions/models/DurableOrchestrationBindings.py +66 -66
- azure/durable_functions/models/DurableOrchestrationClient.py +781 -711
- azure/durable_functions/models/DurableOrchestrationContext.py +722 -707
- azure/durable_functions/models/DurableOrchestrationStatus.py +156 -156
- azure/durable_functions/models/EntityStateResponse.py +23 -23
- azure/durable_functions/models/FunctionContext.py +7 -7
- azure/durable_functions/models/OrchestrationRuntimeStatus.py +32 -29
- azure/durable_functions/models/OrchestratorState.py +117 -116
- azure/durable_functions/models/PurgeHistoryResult.py +33 -33
- azure/durable_functions/models/ReplaySchema.py +8 -8
- azure/durable_functions/models/RetryOptions.py +69 -69
- azure/durable_functions/models/RpcManagementOptions.py +86 -86
- azure/durable_functions/models/Task.py +426 -426
- azure/durable_functions/models/TaskOrchestrationExecutor.py +346 -333
- azure/durable_functions/models/TokenSource.py +56 -56
- azure/durable_functions/models/__init__.py +24 -24
- azure/durable_functions/models/actions/Action.py +23 -23
- azure/durable_functions/models/actions/ActionType.py +18 -18
- azure/durable_functions/models/actions/CallActivityAction.py +41 -41
- azure/durable_functions/models/actions/CallActivityWithRetryAction.py +45 -45
- azure/durable_functions/models/actions/CallEntityAction.py +46 -46
- azure/durable_functions/models/actions/CallHttpAction.py +35 -35
- azure/durable_functions/models/actions/CallSubOrchestratorAction.py +40 -40
- azure/durable_functions/models/actions/CallSubOrchestratorWithRetryAction.py +44 -44
- azure/durable_functions/models/actions/CompoundAction.py +35 -35
- azure/durable_functions/models/actions/ContinueAsNewAction.py +36 -36
- azure/durable_functions/models/actions/CreateTimerAction.py +48 -48
- azure/durable_functions/models/actions/NoOpAction.py +35 -35
- azure/durable_functions/models/actions/SignalEntityAction.py +47 -47
- azure/durable_functions/models/actions/WaitForExternalEventAction.py +63 -63
- azure/durable_functions/models/actions/WhenAllAction.py +14 -14
- azure/durable_functions/models/actions/WhenAnyAction.py +14 -14
- azure/durable_functions/models/actions/__init__.py +24 -24
- azure/durable_functions/models/entities/EntityState.py +74 -74
- azure/durable_functions/models/entities/OperationResult.py +76 -76
- azure/durable_functions/models/entities/RequestMessage.py +53 -53
- azure/durable_functions/models/entities/ResponseMessage.py +48 -48
- azure/durable_functions/models/entities/Signal.py +62 -62
- azure/durable_functions/models/entities/__init__.py +17 -17
- azure/durable_functions/models/history/HistoryEvent.py +92 -92
- azure/durable_functions/models/history/HistoryEventType.py +27 -25
- azure/durable_functions/models/history/__init__.py +8 -8
- azure/durable_functions/models/utils/__init__.py +7 -7
- azure/durable_functions/models/utils/entity_utils.py +103 -91
- azure/durable_functions/models/utils/http_utils.py +69 -69
- azure/durable_functions/models/utils/json_utils.py +56 -56
- azure/durable_functions/orchestrator.py +71 -71
- {azure_functions_durable-1.2.8.dist-info → azure_functions_durable-1.2.10.dist-info}/LICENSE +21 -21
- {azure_functions_durable-1.2.8.dist-info → azure_functions_durable-1.2.10.dist-info}/METADATA +58 -58
- azure_functions_durable-1.2.10.dist-info/RECORD +100 -0
- {azure_functions_durable-1.2.8.dist-info → azure_functions_durable-1.2.10.dist-info}/WHEEL +1 -1
- tests/models/test_DecoratorMetadata.py +135 -135
- tests/models/test_Decorators.py +107 -107
- tests/models/test_DurableOrchestrationBindings.py +68 -56
- tests/models/test_DurableOrchestrationClient.py +730 -612
- tests/models/test_DurableOrchestrationContext.py +102 -102
- tests/models/test_DurableOrchestrationStatus.py +59 -59
- tests/models/test_OrchestrationState.py +28 -28
- tests/models/test_RpcManagementOptions.py +79 -79
- tests/models/test_TokenSource.py +10 -10
- tests/orchestrator/models/OrchestrationInstance.py +18 -18
- tests/orchestrator/orchestrator_test_utils.py +130 -130
- tests/orchestrator/schemas/OrchetrationStateSchema.py +66 -66
- tests/orchestrator/test_call_http.py +235 -176
- tests/orchestrator/test_continue_as_new.py +67 -67
- tests/orchestrator/test_create_timer.py +126 -126
- tests/orchestrator/test_entity.py +395 -395
- tests/orchestrator/test_external_event.py +53 -53
- tests/orchestrator/test_fan_out_fan_in.py +175 -175
- tests/orchestrator/test_is_replaying_flag.py +101 -101
- tests/orchestrator/test_retries.py +308 -308
- tests/orchestrator/test_sequential_orchestrator.py +841 -801
- tests/orchestrator/test_sequential_orchestrator_custom_status.py +119 -119
- tests/orchestrator/test_sequential_orchestrator_with_retry.py +465 -465
- tests/orchestrator/test_serialization.py +30 -30
- tests/orchestrator/test_sub_orchestrator.py +95 -95
- tests/orchestrator/test_sub_orchestrator_with_retry.py +129 -129
- tests/orchestrator/test_task_any.py +60 -60
- tests/tasks/tasks_test_utils.py +17 -17
- tests/tasks/test_new_uuid.py +34 -34
- tests/test_utils/ContextBuilder.py +174 -174
- tests/test_utils/EntityContextBuilder.py +56 -56
- tests/test_utils/constants.py +1 -1
- tests/test_utils/json_utils.py +30 -30
- tests/test_utils/testClasses.py +56 -56
- tests/utils/__init__.py +1 -0
- tests/utils/test_entity_utils.py +24 -0
- azure_functions_durable-1.2.8.data/data/_manifest/bsi.json +0 -1
- azure_functions_durable-1.2.8.data/data/_manifest/manifest.cat +0 -0
- azure_functions_durable-1.2.8.data/data/_manifest/manifest.spdx.json +0 -12845
- azure_functions_durable-1.2.8.data/data/_manifest/manifest.spdx.json.sha256 +0 -1
- azure_functions_durable-1.2.8.dist-info/RECORD +0 -102
- {azure_functions_durable-1.2.8.dist-info → azure_functions_durable-1.2.10.dist-info}/top_level.txt +0 -0
|
@@ -1,707 +1,722 @@
|
|
|
1
|
-
from collections import defaultdict
|
|
2
|
-
from azure.durable_functions.models.actions.SignalEntityAction import SignalEntityAction
|
|
3
|
-
from azure.durable_functions.models.actions.CallEntityAction import CallEntityAction
|
|
4
|
-
from azure.durable_functions.models.Task import TaskBase, TimerTask
|
|
5
|
-
from azure.durable_functions.models.actions.CallHttpAction import CallHttpAction
|
|
6
|
-
from azure.durable_functions.models.DurableHttpRequest import DurableHttpRequest
|
|
7
|
-
from azure.durable_functions.models.actions.CallSubOrchestratorWithRetryAction import \
|
|
8
|
-
CallSubOrchestratorWithRetryAction
|
|
9
|
-
from azure.durable_functions.models.actions.CallActivityWithRetryAction import \
|
|
10
|
-
CallActivityWithRetryAction
|
|
11
|
-
from azure.durable_functions.models.actions.ContinueAsNewAction import \
|
|
12
|
-
ContinueAsNewAction
|
|
13
|
-
from azure.durable_functions.models.actions.WaitForExternalEventAction import \
|
|
14
|
-
WaitForExternalEventAction
|
|
15
|
-
from azure.durable_functions.models.actions.CallSubOrchestratorAction import \
|
|
16
|
-
CallSubOrchestratorAction
|
|
17
|
-
from azure.durable_functions.models.actions.CreateTimerAction import CreateTimerAction
|
|
18
|
-
from azure.durable_functions.models.Task import WhenAllTask, WhenAnyTask, AtomicTask, \
|
|
19
|
-
RetryAbleTask
|
|
20
|
-
from azure.durable_functions.models.actions.CallActivityAction import CallActivityAction
|
|
21
|
-
from azure.durable_functions.models.ReplaySchema import ReplaySchema
|
|
22
|
-
import json
|
|
23
|
-
import datetime
|
|
24
|
-
import inspect
|
|
25
|
-
from typing import DefaultDict, List, Any, Dict, Optional, Tuple, Union, Callable
|
|
26
|
-
from uuid import UUID, uuid5, NAMESPACE_URL, NAMESPACE_OID
|
|
27
|
-
from datetime import timezone
|
|
28
|
-
|
|
29
|
-
from .RetryOptions import RetryOptions
|
|
30
|
-
from .FunctionContext import FunctionContext
|
|
31
|
-
from .history import HistoryEvent, HistoryEventType
|
|
32
|
-
from .actions import Action
|
|
33
|
-
from ..models.TokenSource import TokenSource
|
|
34
|
-
from .utils.entity_utils import EntityId
|
|
35
|
-
from azure.functions._durable_functions import _deserialize_custom_object
|
|
36
|
-
from azure.durable_functions.constants import DATETIME_STRING_FORMAT
|
|
37
|
-
from azure.durable_functions.decorators.metadata import OrchestrationTrigger, ActivityTrigger
|
|
38
|
-
from azure.functions.decorators.function_app import FunctionBuilder
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class DurableOrchestrationContext:
|
|
42
|
-
"""Context of the durable orchestration execution.
|
|
43
|
-
|
|
44
|
-
Parameter data for orchestration bindings that can be used to schedule
|
|
45
|
-
function-based activities.
|
|
46
|
-
"""
|
|
47
|
-
|
|
48
|
-
# parameter names are as defined by JSON schema and do not conform to PEP8 naming conventions
|
|
49
|
-
def __init__(self,
|
|
50
|
-
history: List[Dict[Any, Any]], instanceId: str, isReplaying: bool,
|
|
51
|
-
parentInstanceId: str, input: Any = None, upperSchemaVersion: int = 0, **kwargs):
|
|
52
|
-
self._histories: List[HistoryEvent] = [HistoryEvent(**he) for he in history]
|
|
53
|
-
self._instance_id: str = instanceId
|
|
54
|
-
self._is_replaying: bool = isReplaying
|
|
55
|
-
self._parent_instance_id: str = parentInstanceId
|
|
56
|
-
self._custom_status: Any = None
|
|
57
|
-
self._new_uuid_counter: int = 0
|
|
58
|
-
self._sub_orchestrator_counter: int = 0
|
|
59
|
-
self._continue_as_new_flag: bool = False
|
|
60
|
-
self.decision_started_event: HistoryEvent = \
|
|
61
|
-
[e_ for e_ in self.histories
|
|
62
|
-
if e_.event_type == HistoryEventType.ORCHESTRATOR_STARTED][0]
|
|
63
|
-
self._current_utc_datetime: datetime.datetime = \
|
|
64
|
-
self.decision_started_event.timestamp
|
|
65
|
-
self._new_uuid_counter = 0
|
|
66
|
-
self._function_context: FunctionContext = FunctionContext(**kwargs)
|
|
67
|
-
self._sequence_number = 0
|
|
68
|
-
self._replay_schema = ReplaySchema(upperSchemaVersion)
|
|
69
|
-
|
|
70
|
-
self._action_payload_v1: List[List[Action]] = []
|
|
71
|
-
self._action_payload_v2: List[Action] = []
|
|
72
|
-
|
|
73
|
-
# make _input always a string
|
|
74
|
-
# (consistent with Python Functions generic trigger/input bindings)
|
|
75
|
-
if (isinstance(input, Dict)):
|
|
76
|
-
input = json.dumps(input)
|
|
77
|
-
|
|
78
|
-
self._input: Any = input
|
|
79
|
-
self.open_tasks: DefaultDict[Union[int, str], Union[List[TaskBase], TaskBase]]
|
|
80
|
-
self.open_tasks = defaultdict(list)
|
|
81
|
-
self.deferred_tasks: Dict[Union[int, str], Tuple[HistoryEvent, bool, str]] = {}
|
|
82
|
-
|
|
83
|
-
@classmethod
|
|
84
|
-
def from_json(cls, json_string: str):
|
|
85
|
-
"""Convert the value passed into a new instance of the class.
|
|
86
|
-
|
|
87
|
-
Parameters
|
|
88
|
-
----------
|
|
89
|
-
json_string: str
|
|
90
|
-
Context passed a JSON serializable value to be converted into an instance of the class
|
|
91
|
-
|
|
92
|
-
Returns
|
|
93
|
-
-------
|
|
94
|
-
DurableOrchestrationContext
|
|
95
|
-
New instance of the durable orchestration context class
|
|
96
|
-
"""
|
|
97
|
-
# We should consider parsing the `Input` field here as well,
|
|
98
|
-
# instead of doing so lazily when `get_input` is called.
|
|
99
|
-
json_dict = json.loads(json_string)
|
|
100
|
-
return cls(**json_dict)
|
|
101
|
-
|
|
102
|
-
def _generate_task(self, action: Action,
|
|
103
|
-
retry_options: Optional[RetryOptions] = None,
|
|
104
|
-
id_: Optional[Union[int, str]] = None,
|
|
105
|
-
parent: Optional[TaskBase] = None,
|
|
106
|
-
task_constructor=AtomicTask) -> Union[AtomicTask, RetryAbleTask, TimerTask]:
|
|
107
|
-
"""Generate an atomic or retryable Task based on an input.
|
|
108
|
-
|
|
109
|
-
Parameters
|
|
110
|
-
----------
|
|
111
|
-
action : Action
|
|
112
|
-
The action backing the Task.
|
|
113
|
-
retry_options : Optional[RetryOptions]
|
|
114
|
-
RetryOptions for a with-retry task, by default None
|
|
115
|
-
|
|
116
|
-
Returns
|
|
117
|
-
-------
|
|
118
|
-
Union[AtomicTask, RetryAbleTask]
|
|
119
|
-
Either an atomic task or a retry-able task
|
|
120
|
-
"""
|
|
121
|
-
# Create an atomic task
|
|
122
|
-
task: Union[AtomicTask, RetryAbleTask]
|
|
123
|
-
action_payload: Union[Action, List[Action]]
|
|
124
|
-
|
|
125
|
-
# TODO: find cleanear way to do this
|
|
126
|
-
if self._replay_schema is ReplaySchema.V1:
|
|
127
|
-
action_payload = [action]
|
|
128
|
-
else:
|
|
129
|
-
action_payload = action
|
|
130
|
-
task = task_constructor(id_, action_payload)
|
|
131
|
-
task.parent = parent
|
|
132
|
-
|
|
133
|
-
# if task is retryable, provide the retryable wrapper class
|
|
134
|
-
if not (retry_options is None):
|
|
135
|
-
task = RetryAbleTask(task, retry_options, self)
|
|
136
|
-
return task
|
|
137
|
-
|
|
138
|
-
def _set_is_replaying(self, is_replaying: bool):
|
|
139
|
-
"""Set the internal `is_replaying` flag.
|
|
140
|
-
|
|
141
|
-
Parameters
|
|
142
|
-
----------
|
|
143
|
-
is_replaying : bool
|
|
144
|
-
New value of the `is_replaying` flag
|
|
145
|
-
"""
|
|
146
|
-
self._is_replaying = is_replaying
|
|
147
|
-
|
|
148
|
-
def call_activity(self, name: Union[str, Callable], input_: Optional[Any] = None) -> TaskBase:
|
|
149
|
-
"""Schedule an activity for execution.
|
|
150
|
-
|
|
151
|
-
Parameters
|
|
152
|
-
----------
|
|
153
|
-
name: str | Callable
|
|
154
|
-
Either the name of the activity function to call, as a string or,
|
|
155
|
-
in the Python V2 programming model, the activity function itself.
|
|
156
|
-
input_: Optional[Any]
|
|
157
|
-
The JSON-serializable input to pass to the activity function.
|
|
158
|
-
|
|
159
|
-
Returns
|
|
160
|
-
-------
|
|
161
|
-
Task
|
|
162
|
-
A Durable Task that completes when the called activity function completes or fails.
|
|
163
|
-
"""
|
|
164
|
-
if isinstance(name, Callable) and not isinstance(name, FunctionBuilder):
|
|
165
|
-
error_message = "The `call_activity` API received a `Callable` without an "\
|
|
166
|
-
"associated Azure Functions trigger-type. "\
|
|
167
|
-
"Please ensure you're using the Python programming model V2 "\
|
|
168
|
-
"and that your activity function is annotated with the `activity_trigger`"\
|
|
169
|
-
"decorator. Otherwise, provide in the name of the activity as a string."
|
|
170
|
-
raise ValueError(error_message)
|
|
171
|
-
|
|
172
|
-
if isinstance(name, FunctionBuilder):
|
|
173
|
-
name = self._get_function_name(name, ActivityTrigger)
|
|
174
|
-
|
|
175
|
-
action = CallActivityAction(name, input_)
|
|
176
|
-
task = self._generate_task(action)
|
|
177
|
-
return task
|
|
178
|
-
|
|
179
|
-
def call_activity_with_retry(self,
|
|
180
|
-
name: Union[str, Callable], retry_options: RetryOptions,
|
|
181
|
-
input_: Optional[Any] = None) -> TaskBase:
|
|
182
|
-
"""Schedule an activity for execution with retry options.
|
|
183
|
-
|
|
184
|
-
Parameters
|
|
185
|
-
----------
|
|
186
|
-
name: str | Callable
|
|
187
|
-
Either the name of the activity function to call, as a string or,
|
|
188
|
-
in the Python V2 programming model, the activity function itself.
|
|
189
|
-
retry_options: RetryOptions
|
|
190
|
-
The retry options for the activity function.
|
|
191
|
-
input_: Optional[Any]
|
|
192
|
-
The JSON-serializable input to pass to the activity function.
|
|
193
|
-
|
|
194
|
-
Returns
|
|
195
|
-
-------
|
|
196
|
-
Task
|
|
197
|
-
A Durable Task that completes when the called activity function completes or
|
|
198
|
-
fails completely.
|
|
199
|
-
"""
|
|
200
|
-
if isinstance(name, Callable) and not isinstance(name, FunctionBuilder):
|
|
201
|
-
error_message = "The `call_activity` API received a `Callable` without an "\
|
|
202
|
-
"associated Azure Functions trigger-type. "\
|
|
203
|
-
"Please ensure you're using the Python programming model V2 "\
|
|
204
|
-
"and that your activity function is annotated with the `activity_trigger`"\
|
|
205
|
-
"decorator. Otherwise, provide in the name of the activity as a string."
|
|
206
|
-
raise ValueError(error_message)
|
|
207
|
-
|
|
208
|
-
if isinstance(name, FunctionBuilder):
|
|
209
|
-
name = self._get_function_name(name, ActivityTrigger)
|
|
210
|
-
|
|
211
|
-
action = CallActivityWithRetryAction(name, retry_options, input_)
|
|
212
|
-
task = self._generate_task(action, retry_options)
|
|
213
|
-
return task
|
|
214
|
-
|
|
215
|
-
def call_http(self, method: str, uri: str, content: Optional[str] = None,
|
|
216
|
-
headers: Optional[Dict[str, str]] = None,
|
|
217
|
-
token_source: TokenSource = None
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
"""
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
name
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
return
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
"""
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
"""
|
|
423
|
-
return self.
|
|
424
|
-
|
|
425
|
-
@property
|
|
426
|
-
def
|
|
427
|
-
"""Get the
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
Returns
|
|
468
|
-
-------
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
"""
|
|
472
|
-
return self.
|
|
473
|
-
|
|
474
|
-
@
|
|
475
|
-
def current_utc_datetime(self
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
Returns
|
|
483
|
-
-------
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
"""
|
|
487
|
-
return self.
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
"""
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
"""
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
"""
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
def
|
|
617
|
-
"""
|
|
618
|
-
|
|
619
|
-
Returns
|
|
620
|
-
-------
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
"""
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
if self.
|
|
640
|
-
return
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
"""
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from azure.durable_functions.models.actions.SignalEntityAction import SignalEntityAction
|
|
3
|
+
from azure.durable_functions.models.actions.CallEntityAction import CallEntityAction
|
|
4
|
+
from azure.durable_functions.models.Task import TaskBase, TimerTask
|
|
5
|
+
from azure.durable_functions.models.actions.CallHttpAction import CallHttpAction
|
|
6
|
+
from azure.durable_functions.models.DurableHttpRequest import DurableHttpRequest
|
|
7
|
+
from azure.durable_functions.models.actions.CallSubOrchestratorWithRetryAction import \
|
|
8
|
+
CallSubOrchestratorWithRetryAction
|
|
9
|
+
from azure.durable_functions.models.actions.CallActivityWithRetryAction import \
|
|
10
|
+
CallActivityWithRetryAction
|
|
11
|
+
from azure.durable_functions.models.actions.ContinueAsNewAction import \
|
|
12
|
+
ContinueAsNewAction
|
|
13
|
+
from azure.durable_functions.models.actions.WaitForExternalEventAction import \
|
|
14
|
+
WaitForExternalEventAction
|
|
15
|
+
from azure.durable_functions.models.actions.CallSubOrchestratorAction import \
|
|
16
|
+
CallSubOrchestratorAction
|
|
17
|
+
from azure.durable_functions.models.actions.CreateTimerAction import CreateTimerAction
|
|
18
|
+
from azure.durable_functions.models.Task import WhenAllTask, WhenAnyTask, AtomicTask, \
|
|
19
|
+
RetryAbleTask
|
|
20
|
+
from azure.durable_functions.models.actions.CallActivityAction import CallActivityAction
|
|
21
|
+
from azure.durable_functions.models.ReplaySchema import ReplaySchema
|
|
22
|
+
import json
|
|
23
|
+
import datetime
|
|
24
|
+
import inspect
|
|
25
|
+
from typing import DefaultDict, List, Any, Dict, Optional, Tuple, Union, Callable
|
|
26
|
+
from uuid import UUID, uuid5, NAMESPACE_URL, NAMESPACE_OID
|
|
27
|
+
from datetime import timezone
|
|
28
|
+
|
|
29
|
+
from .RetryOptions import RetryOptions
|
|
30
|
+
from .FunctionContext import FunctionContext
|
|
31
|
+
from .history import HistoryEvent, HistoryEventType
|
|
32
|
+
from .actions import Action
|
|
33
|
+
from ..models.TokenSource import TokenSource
|
|
34
|
+
from .utils.entity_utils import EntityId
|
|
35
|
+
from azure.functions._durable_functions import _deserialize_custom_object
|
|
36
|
+
from azure.durable_functions.constants import DATETIME_STRING_FORMAT
|
|
37
|
+
from azure.durable_functions.decorators.metadata import OrchestrationTrigger, ActivityTrigger
|
|
38
|
+
from azure.functions.decorators.function_app import FunctionBuilder
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class DurableOrchestrationContext:
|
|
42
|
+
"""Context of the durable orchestration execution.
|
|
43
|
+
|
|
44
|
+
Parameter data for orchestration bindings that can be used to schedule
|
|
45
|
+
function-based activities.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
# parameter names are as defined by JSON schema and do not conform to PEP8 naming conventions
|
|
49
|
+
def __init__(self,
|
|
50
|
+
history: List[Dict[Any, Any]], instanceId: str, isReplaying: bool,
|
|
51
|
+
parentInstanceId: str, input: Any = None, upperSchemaVersion: int = 0, **kwargs):
|
|
52
|
+
self._histories: List[HistoryEvent] = [HistoryEvent(**he) for he in history]
|
|
53
|
+
self._instance_id: str = instanceId
|
|
54
|
+
self._is_replaying: bool = isReplaying
|
|
55
|
+
self._parent_instance_id: str = parentInstanceId
|
|
56
|
+
self._custom_status: Any = None
|
|
57
|
+
self._new_uuid_counter: int = 0
|
|
58
|
+
self._sub_orchestrator_counter: int = 0
|
|
59
|
+
self._continue_as_new_flag: bool = False
|
|
60
|
+
self.decision_started_event: HistoryEvent = \
|
|
61
|
+
[e_ for e_ in self.histories
|
|
62
|
+
if e_.event_type == HistoryEventType.ORCHESTRATOR_STARTED][0]
|
|
63
|
+
self._current_utc_datetime: datetime.datetime = \
|
|
64
|
+
self.decision_started_event.timestamp
|
|
65
|
+
self._new_uuid_counter = 0
|
|
66
|
+
self._function_context: FunctionContext = FunctionContext(**kwargs)
|
|
67
|
+
self._sequence_number = 0
|
|
68
|
+
self._replay_schema = ReplaySchema(upperSchemaVersion)
|
|
69
|
+
|
|
70
|
+
self._action_payload_v1: List[List[Action]] = []
|
|
71
|
+
self._action_payload_v2: List[Action] = []
|
|
72
|
+
|
|
73
|
+
# make _input always a string
|
|
74
|
+
# (consistent with Python Functions generic trigger/input bindings)
|
|
75
|
+
if (isinstance(input, Dict)):
|
|
76
|
+
input = json.dumps(input)
|
|
77
|
+
|
|
78
|
+
self._input: Any = input
|
|
79
|
+
self.open_tasks: DefaultDict[Union[int, str], Union[List[TaskBase], TaskBase]]
|
|
80
|
+
self.open_tasks = defaultdict(list)
|
|
81
|
+
self.deferred_tasks: Dict[Union[int, str], Tuple[HistoryEvent, bool, str]] = {}
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def from_json(cls, json_string: str):
|
|
85
|
+
"""Convert the value passed into a new instance of the class.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
json_string: str
|
|
90
|
+
Context passed a JSON serializable value to be converted into an instance of the class
|
|
91
|
+
|
|
92
|
+
Returns
|
|
93
|
+
-------
|
|
94
|
+
DurableOrchestrationContext
|
|
95
|
+
New instance of the durable orchestration context class
|
|
96
|
+
"""
|
|
97
|
+
# We should consider parsing the `Input` field here as well,
|
|
98
|
+
# instead of doing so lazily when `get_input` is called.
|
|
99
|
+
json_dict = json.loads(json_string)
|
|
100
|
+
return cls(**json_dict)
|
|
101
|
+
|
|
102
|
+
def _generate_task(self, action: Action,
|
|
103
|
+
retry_options: Optional[RetryOptions] = None,
|
|
104
|
+
id_: Optional[Union[int, str]] = None,
|
|
105
|
+
parent: Optional[TaskBase] = None,
|
|
106
|
+
task_constructor=AtomicTask) -> Union[AtomicTask, RetryAbleTask, TimerTask]:
|
|
107
|
+
"""Generate an atomic or retryable Task based on an input.
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
action : Action
|
|
112
|
+
The action backing the Task.
|
|
113
|
+
retry_options : Optional[RetryOptions]
|
|
114
|
+
RetryOptions for a with-retry task, by default None
|
|
115
|
+
|
|
116
|
+
Returns
|
|
117
|
+
-------
|
|
118
|
+
Union[AtomicTask, RetryAbleTask]
|
|
119
|
+
Either an atomic task or a retry-able task
|
|
120
|
+
"""
|
|
121
|
+
# Create an atomic task
|
|
122
|
+
task: Union[AtomicTask, RetryAbleTask]
|
|
123
|
+
action_payload: Union[Action, List[Action]]
|
|
124
|
+
|
|
125
|
+
# TODO: find cleanear way to do this
|
|
126
|
+
if self._replay_schema is ReplaySchema.V1:
|
|
127
|
+
action_payload = [action]
|
|
128
|
+
else:
|
|
129
|
+
action_payload = action
|
|
130
|
+
task = task_constructor(id_, action_payload)
|
|
131
|
+
task.parent = parent
|
|
132
|
+
|
|
133
|
+
# if task is retryable, provide the retryable wrapper class
|
|
134
|
+
if not (retry_options is None):
|
|
135
|
+
task = RetryAbleTask(task, retry_options, self)
|
|
136
|
+
return task
|
|
137
|
+
|
|
138
|
+
def _set_is_replaying(self, is_replaying: bool):
|
|
139
|
+
"""Set the internal `is_replaying` flag.
|
|
140
|
+
|
|
141
|
+
Parameters
|
|
142
|
+
----------
|
|
143
|
+
is_replaying : bool
|
|
144
|
+
New value of the `is_replaying` flag
|
|
145
|
+
"""
|
|
146
|
+
self._is_replaying = is_replaying
|
|
147
|
+
|
|
148
|
+
def call_activity(self, name: Union[str, Callable], input_: Optional[Any] = None) -> TaskBase:
|
|
149
|
+
"""Schedule an activity for execution.
|
|
150
|
+
|
|
151
|
+
Parameters
|
|
152
|
+
----------
|
|
153
|
+
name: str | Callable
|
|
154
|
+
Either the name of the activity function to call, as a string or,
|
|
155
|
+
in the Python V2 programming model, the activity function itself.
|
|
156
|
+
input_: Optional[Any]
|
|
157
|
+
The JSON-serializable input to pass to the activity function.
|
|
158
|
+
|
|
159
|
+
Returns
|
|
160
|
+
-------
|
|
161
|
+
Task
|
|
162
|
+
A Durable Task that completes when the called activity function completes or fails.
|
|
163
|
+
"""
|
|
164
|
+
if isinstance(name, Callable) and not isinstance(name, FunctionBuilder):
|
|
165
|
+
error_message = "The `call_activity` API received a `Callable` without an "\
|
|
166
|
+
"associated Azure Functions trigger-type. "\
|
|
167
|
+
"Please ensure you're using the Python programming model V2 "\
|
|
168
|
+
"and that your activity function is annotated with the `activity_trigger`"\
|
|
169
|
+
"decorator. Otherwise, provide in the name of the activity as a string."
|
|
170
|
+
raise ValueError(error_message)
|
|
171
|
+
|
|
172
|
+
if isinstance(name, FunctionBuilder):
|
|
173
|
+
name = self._get_function_name(name, ActivityTrigger)
|
|
174
|
+
|
|
175
|
+
action = CallActivityAction(name, input_)
|
|
176
|
+
task = self._generate_task(action)
|
|
177
|
+
return task
|
|
178
|
+
|
|
179
|
+
def call_activity_with_retry(self,
|
|
180
|
+
name: Union[str, Callable], retry_options: RetryOptions,
|
|
181
|
+
input_: Optional[Any] = None) -> TaskBase:
|
|
182
|
+
"""Schedule an activity for execution with retry options.
|
|
183
|
+
|
|
184
|
+
Parameters
|
|
185
|
+
----------
|
|
186
|
+
name: str | Callable
|
|
187
|
+
Either the name of the activity function to call, as a string or,
|
|
188
|
+
in the Python V2 programming model, the activity function itself.
|
|
189
|
+
retry_options: RetryOptions
|
|
190
|
+
The retry options for the activity function.
|
|
191
|
+
input_: Optional[Any]
|
|
192
|
+
The JSON-serializable input to pass to the activity function.
|
|
193
|
+
|
|
194
|
+
Returns
|
|
195
|
+
-------
|
|
196
|
+
Task
|
|
197
|
+
A Durable Task that completes when the called activity function completes or
|
|
198
|
+
fails completely.
|
|
199
|
+
"""
|
|
200
|
+
if isinstance(name, Callable) and not isinstance(name, FunctionBuilder):
|
|
201
|
+
error_message = "The `call_activity` API received a `Callable` without an "\
|
|
202
|
+
"associated Azure Functions trigger-type. "\
|
|
203
|
+
"Please ensure you're using the Python programming model V2 "\
|
|
204
|
+
"and that your activity function is annotated with the `activity_trigger`"\
|
|
205
|
+
"decorator. Otherwise, provide in the name of the activity as a string."
|
|
206
|
+
raise ValueError(error_message)
|
|
207
|
+
|
|
208
|
+
if isinstance(name, FunctionBuilder):
|
|
209
|
+
name = self._get_function_name(name, ActivityTrigger)
|
|
210
|
+
|
|
211
|
+
action = CallActivityWithRetryAction(name, retry_options, input_)
|
|
212
|
+
task = self._generate_task(action, retry_options)
|
|
213
|
+
return task
|
|
214
|
+
|
|
215
|
+
def call_http(self, method: str, uri: str, content: Optional[str] = None,
|
|
216
|
+
headers: Optional[Dict[str, str]] = None,
|
|
217
|
+
token_source: TokenSource = None,
|
|
218
|
+
is_raw_str: bool = False) -> TaskBase:
|
|
219
|
+
"""Schedule a durable HTTP call to the specified endpoint.
|
|
220
|
+
|
|
221
|
+
Parameters
|
|
222
|
+
----------
|
|
223
|
+
method: str
|
|
224
|
+
The HTTP request method.
|
|
225
|
+
uri: str
|
|
226
|
+
The HTTP request uri.
|
|
227
|
+
content: Optional[str]
|
|
228
|
+
The HTTP request content.
|
|
229
|
+
headers: Optional[Dict[str, str]]
|
|
230
|
+
The HTTP request headers.
|
|
231
|
+
token_source: TokenSource
|
|
232
|
+
The source of OAuth token to add to the request.
|
|
233
|
+
is_raw_str: bool, optional
|
|
234
|
+
If True, send string content as-is.
|
|
235
|
+
If False (default), serialize content to JSON.
|
|
236
|
+
|
|
237
|
+
Returns
|
|
238
|
+
-------
|
|
239
|
+
Task
|
|
240
|
+
The durable HTTP request to schedule.
|
|
241
|
+
"""
|
|
242
|
+
json_content: Optional[str] = None
|
|
243
|
+
|
|
244
|
+
# validate parameters
|
|
245
|
+
if (not isinstance(content, str)) and is_raw_str:
|
|
246
|
+
raise TypeError(
|
|
247
|
+
"Invalid use of 'is_raw_str' parameter: 'is_raw_str' is "
|
|
248
|
+
"set to 'True' but 'content' is not an instance of type 'str'. "
|
|
249
|
+
"Either set 'is_raw_str' to 'False', or ensure your 'content' "
|
|
250
|
+
"is of type 'str'.")
|
|
251
|
+
|
|
252
|
+
if content is not None:
|
|
253
|
+
if isinstance(content, str) and is_raw_str:
|
|
254
|
+
# don't serialize the str value - use it as the raw HTTP request payload
|
|
255
|
+
json_content = content
|
|
256
|
+
else:
|
|
257
|
+
json_content = json.dumps(content)
|
|
258
|
+
|
|
259
|
+
request = DurableHttpRequest(method, uri, json_content, headers, token_source)
|
|
260
|
+
action = CallHttpAction(request)
|
|
261
|
+
task = self._generate_task(action)
|
|
262
|
+
return task
|
|
263
|
+
|
|
264
|
+
def call_sub_orchestrator(self,
|
|
265
|
+
name: Union[str, Callable], input_: Optional[Any] = None,
|
|
266
|
+
instance_id: Optional[str] = None) -> TaskBase:
|
|
267
|
+
"""Schedule sub-orchestration function named `name` for execution.
|
|
268
|
+
|
|
269
|
+
Parameters
|
|
270
|
+
----------
|
|
271
|
+
name: Union[str, Callable]
|
|
272
|
+
The name of the orchestrator function to call.
|
|
273
|
+
input_: Optional[Any]
|
|
274
|
+
The JSON-serializable input to pass to the orchestrator function.
|
|
275
|
+
instance_id: Optional[str]
|
|
276
|
+
A unique ID to use for the sub-orchestration instance.
|
|
277
|
+
|
|
278
|
+
Returns
|
|
279
|
+
-------
|
|
280
|
+
Task
|
|
281
|
+
A Durable Task that completes when the called sub-orchestrator completes or fails.
|
|
282
|
+
"""
|
|
283
|
+
if isinstance(name, Callable) and not isinstance(name, FunctionBuilder):
|
|
284
|
+
error_message = "The `call_activity` API received a `Callable` without an "\
|
|
285
|
+
"associated Azure Functions trigger-type. "\
|
|
286
|
+
"Please ensure you're using the Python programming model V2 "\
|
|
287
|
+
"and that your activity function is annotated with the `activity_trigger`"\
|
|
288
|
+
"decorator. Otherwise, provide in the name of the activity as a string."
|
|
289
|
+
raise ValueError(error_message)
|
|
290
|
+
|
|
291
|
+
if isinstance(name, FunctionBuilder):
|
|
292
|
+
name = self._get_function_name(name, OrchestrationTrigger)
|
|
293
|
+
|
|
294
|
+
action = CallSubOrchestratorAction(name, input_, instance_id)
|
|
295
|
+
task = self._generate_task(action)
|
|
296
|
+
return task
|
|
297
|
+
|
|
298
|
+
def call_sub_orchestrator_with_retry(self,
|
|
299
|
+
name: Union[str, Callable], retry_options: RetryOptions,
|
|
300
|
+
input_: Optional[Any] = None,
|
|
301
|
+
instance_id: Optional[str] = None) -> TaskBase:
|
|
302
|
+
"""Schedule sub-orchestration function named `name` for execution, with retry-options.
|
|
303
|
+
|
|
304
|
+
Parameters
|
|
305
|
+
----------
|
|
306
|
+
name: Union[str, Callable]
|
|
307
|
+
The name of the activity function to schedule.
|
|
308
|
+
retry_options: RetryOptions
|
|
309
|
+
The settings for retrying this sub-orchestrator in case of a failure.
|
|
310
|
+
input_: Optional[Any]
|
|
311
|
+
The JSON-serializable input to pass to the activity function. Defaults to None.
|
|
312
|
+
instance_id: str
|
|
313
|
+
The instance ID of the sub-orchestrator to call.
|
|
314
|
+
|
|
315
|
+
Returns
|
|
316
|
+
-------
|
|
317
|
+
Task
|
|
318
|
+
A Durable Task that completes when the called sub-orchestrator completes or fails.
|
|
319
|
+
"""
|
|
320
|
+
if isinstance(name, Callable) and not isinstance(name, FunctionBuilder):
|
|
321
|
+
error_message = "The `call_activity` API received a `Callable` without an "\
|
|
322
|
+
"associated Azure Functions trigger-type. "\
|
|
323
|
+
"Please ensure you're using the Python programming model V2 "\
|
|
324
|
+
"and that your activity function is annotated with the `activity_trigger`"\
|
|
325
|
+
"decorator. Otherwise, provide in the name of the activity as a string."
|
|
326
|
+
raise ValueError(error_message)
|
|
327
|
+
|
|
328
|
+
if isinstance(name, FunctionBuilder):
|
|
329
|
+
name = self._get_function_name(name, OrchestrationTrigger)
|
|
330
|
+
|
|
331
|
+
action = CallSubOrchestratorWithRetryAction(name, retry_options, input_, instance_id)
|
|
332
|
+
task = self._generate_task(action, retry_options)
|
|
333
|
+
return task
|
|
334
|
+
|
|
335
|
+
def get_input(self) -> Optional[Any]:
|
|
336
|
+
"""Get the orchestration input."""
|
|
337
|
+
return None if self._input is None else json.loads(self._input,
|
|
338
|
+
object_hook=_deserialize_custom_object)
|
|
339
|
+
|
|
340
|
+
def new_uuid(self) -> str:
|
|
341
|
+
"""Create a new UUID that is safe for replay within an orchestration or operation.
|
|
342
|
+
|
|
343
|
+
The default implementation of this method creates a name-based UUID
|
|
344
|
+
using the algorithm from RFC 4122 §4.3. The name input used to generate
|
|
345
|
+
this value is a combination of the orchestration instance ID and an
|
|
346
|
+
internally managed sequence number.
|
|
347
|
+
|
|
348
|
+
Returns
|
|
349
|
+
-------
|
|
350
|
+
str
|
|
351
|
+
New UUID that is safe for replay within an orchestration or operation.
|
|
352
|
+
"""
|
|
353
|
+
URL_NAMESPACE: str = "9e952958-5e33-4daf-827f-2fa12937b875"
|
|
354
|
+
|
|
355
|
+
uuid_name_value = \
|
|
356
|
+
f"{self._instance_id}" \
|
|
357
|
+
f"_{self.current_utc_datetime.strftime(DATETIME_STRING_FORMAT)}" \
|
|
358
|
+
f"_{self._new_uuid_counter}"
|
|
359
|
+
self._new_uuid_counter += 1
|
|
360
|
+
namespace_uuid = uuid5(NAMESPACE_OID, URL_NAMESPACE)
|
|
361
|
+
return str(uuid5(namespace_uuid, uuid_name_value))
|
|
362
|
+
|
|
363
|
+
def task_all(self, activities: List[TaskBase]) -> TaskBase:
|
|
364
|
+
"""Schedule the execution of all activities.
|
|
365
|
+
|
|
366
|
+
Similar to Promise.all. When called with `yield` or `return`, returns an
|
|
367
|
+
array containing the results of all [[Task]]s passed to it. It returns
|
|
368
|
+
when all of the [[Task]] instances have completed.
|
|
369
|
+
|
|
370
|
+
Throws an exception if any of the activities fails
|
|
371
|
+
Parameters
|
|
372
|
+
----------
|
|
373
|
+
activities: List[Task]
|
|
374
|
+
List of activities to schedule
|
|
375
|
+
|
|
376
|
+
Returns
|
|
377
|
+
-------
|
|
378
|
+
TaskSet
|
|
379
|
+
The results of all activities.
|
|
380
|
+
"""
|
|
381
|
+
return WhenAllTask(activities, replay_schema=self._replay_schema)
|
|
382
|
+
|
|
383
|
+
def task_any(self, activities: List[TaskBase]) -> TaskBase:
|
|
384
|
+
"""Schedule the execution of all activities.
|
|
385
|
+
|
|
386
|
+
Similar to Promise.race. When called with `yield` or `return`, returns
|
|
387
|
+
the first [[Task]] instance to complete.
|
|
388
|
+
|
|
389
|
+
Throws an exception if all of the activities fail
|
|
390
|
+
|
|
391
|
+
Parameters
|
|
392
|
+
----------
|
|
393
|
+
activities: List[Task]
|
|
394
|
+
List of activities to schedule
|
|
395
|
+
|
|
396
|
+
Returns
|
|
397
|
+
-------
|
|
398
|
+
TaskSet
|
|
399
|
+
The first [[Task]] instance to complete.
|
|
400
|
+
"""
|
|
401
|
+
return WhenAnyTask(activities, replay_schema=self._replay_schema)
|
|
402
|
+
|
|
403
|
+
def set_custom_status(self, status: Any):
|
|
404
|
+
"""Set the customized orchestration status for your orchestrator function.
|
|
405
|
+
|
|
406
|
+
This status is also returned by the orchestration client through the get_status API
|
|
407
|
+
|
|
408
|
+
Parameters
|
|
409
|
+
----------
|
|
410
|
+
status : str
|
|
411
|
+
Customized status provided by the orchestrator
|
|
412
|
+
"""
|
|
413
|
+
self._custom_status = status
|
|
414
|
+
|
|
415
|
+
@property
|
|
416
|
+
def custom_status(self):
|
|
417
|
+
"""Get customized status of current orchestration."""
|
|
418
|
+
return self._custom_status
|
|
419
|
+
|
|
420
|
+
@property
|
|
421
|
+
def histories(self):
|
|
422
|
+
"""Get running history of tasks that have been scheduled."""
|
|
423
|
+
return self._histories
|
|
424
|
+
|
|
425
|
+
@property
|
|
426
|
+
def instance_id(self) -> str:
|
|
427
|
+
"""Get the ID of the current orchestration instance.
|
|
428
|
+
|
|
429
|
+
The instance ID is generated and fixed when the orchestrator function
|
|
430
|
+
is scheduled. It can be either auto-generated, in which case it is
|
|
431
|
+
formatted as a GUID, or it can be user-specified with any format.
|
|
432
|
+
|
|
433
|
+
Returns
|
|
434
|
+
-------
|
|
435
|
+
str
|
|
436
|
+
The ID of the current orchestration instance.
|
|
437
|
+
"""
|
|
438
|
+
return self._instance_id
|
|
439
|
+
|
|
440
|
+
@property
|
|
441
|
+
def is_replaying(self) -> bool:
|
|
442
|
+
"""Get the value indicating orchestration replaying itself.
|
|
443
|
+
|
|
444
|
+
This property is useful when there is logic that needs to run only when
|
|
445
|
+
the orchestrator function is _not_ replaying. For example, certain
|
|
446
|
+
types of application logging may become too noisy when duplicated as
|
|
447
|
+
part of orchestrator function replay. The orchestrator code could check
|
|
448
|
+
to see whether the function is being replayed and then issue the log
|
|
449
|
+
statements when this value is `false`.
|
|
450
|
+
|
|
451
|
+
Returns
|
|
452
|
+
-------
|
|
453
|
+
bool
|
|
454
|
+
Value indicating whether the orchestrator function is currently replaying.
|
|
455
|
+
"""
|
|
456
|
+
return self._is_replaying
|
|
457
|
+
|
|
458
|
+
@property
|
|
459
|
+
def parent_instance_id(self) -> str:
|
|
460
|
+
"""Get the ID of the parent orchestration.
|
|
461
|
+
|
|
462
|
+
The parent instance ID is generated and fixed when the parent
|
|
463
|
+
orchestrator function is scheduled. It can be either auto-generated, in
|
|
464
|
+
which case it is formatted as a GUID, or it can be user-specified with
|
|
465
|
+
any format.
|
|
466
|
+
|
|
467
|
+
Returns
|
|
468
|
+
-------
|
|
469
|
+
str
|
|
470
|
+
ID of the parent orchestration of the current sub-orchestration instance
|
|
471
|
+
"""
|
|
472
|
+
return self._parent_instance_id
|
|
473
|
+
|
|
474
|
+
@property
|
|
475
|
+
def current_utc_datetime(self) -> datetime.datetime:
|
|
476
|
+
"""Get the current date/time.
|
|
477
|
+
|
|
478
|
+
This date/time value is derived from the orchestration history. It
|
|
479
|
+
always returns the same value at specific points in the orchestrator
|
|
480
|
+
function code, making it deterministic and safe for replay.
|
|
481
|
+
|
|
482
|
+
Returns
|
|
483
|
+
-------
|
|
484
|
+
datetime
|
|
485
|
+
The current date/time in a way that is safe for use by orchestrator functions
|
|
486
|
+
"""
|
|
487
|
+
return self._current_utc_datetime
|
|
488
|
+
|
|
489
|
+
@current_utc_datetime.setter
|
|
490
|
+
def current_utc_datetime(self, value: datetime.datetime):
|
|
491
|
+
self._current_utc_datetime = value
|
|
492
|
+
|
|
493
|
+
@property
|
|
494
|
+
def function_context(self) -> FunctionContext:
|
|
495
|
+
"""Get the function level attributes not used by durable orchestrator.
|
|
496
|
+
|
|
497
|
+
Returns
|
|
498
|
+
-------
|
|
499
|
+
FunctionContext
|
|
500
|
+
Object containing function level attributes not used by durable orchestrator.
|
|
501
|
+
"""
|
|
502
|
+
return self._function_context
|
|
503
|
+
|
|
504
|
+
def call_entity(self, entityId: EntityId,
|
|
505
|
+
operationName: str, operationInput: Optional[Any] = None):
|
|
506
|
+
"""Get the result of Durable Entity operation given some input.
|
|
507
|
+
|
|
508
|
+
Parameters
|
|
509
|
+
----------
|
|
510
|
+
entityId: EntityId
|
|
511
|
+
The ID of the entity to call
|
|
512
|
+
operationName: str
|
|
513
|
+
The operation to execute
|
|
514
|
+
operationInput: Optional[Any]
|
|
515
|
+
The input for tne operation, defaults to None.
|
|
516
|
+
|
|
517
|
+
Returns
|
|
518
|
+
-------
|
|
519
|
+
Task
|
|
520
|
+
A Task of the entity call
|
|
521
|
+
"""
|
|
522
|
+
action = CallEntityAction(entityId, operationName, operationInput)
|
|
523
|
+
task = self._generate_task(action)
|
|
524
|
+
return task
|
|
525
|
+
|
|
526
|
+
def _record_fire_and_forget_action(self, action: Action):
|
|
527
|
+
"""Append a responseless-API action object to the actions array.
|
|
528
|
+
|
|
529
|
+
Parameters
|
|
530
|
+
----------
|
|
531
|
+
action : Action
|
|
532
|
+
The action to append
|
|
533
|
+
"""
|
|
534
|
+
new_action: Union[List[Action], Action]
|
|
535
|
+
if self._replay_schema is ReplaySchema.V2:
|
|
536
|
+
new_action = action
|
|
537
|
+
else:
|
|
538
|
+
new_action = [action]
|
|
539
|
+
self._add_to_actions(new_action)
|
|
540
|
+
self._sequence_number += 1
|
|
541
|
+
|
|
542
|
+
def signal_entity(self, entityId: EntityId,
|
|
543
|
+
operationName: str, operationInput: Optional[Any] = None):
|
|
544
|
+
"""Send a signal operation to Durable Entity given some input.
|
|
545
|
+
|
|
546
|
+
Parameters
|
|
547
|
+
----------
|
|
548
|
+
entityId: EntityId
|
|
549
|
+
The ID of the entity to call
|
|
550
|
+
operationName: str
|
|
551
|
+
The operation to execute
|
|
552
|
+
operationInput: Optional[Any]
|
|
553
|
+
The input for tne operation, defaults to None.
|
|
554
|
+
|
|
555
|
+
Returns
|
|
556
|
+
-------
|
|
557
|
+
Task
|
|
558
|
+
A Task of the entity signal
|
|
559
|
+
"""
|
|
560
|
+
action = SignalEntityAction(entityId, operationName, operationInput)
|
|
561
|
+
task = self._generate_task(action)
|
|
562
|
+
self._record_fire_and_forget_action(action)
|
|
563
|
+
return task
|
|
564
|
+
|
|
565
|
+
@property
|
|
566
|
+
def will_continue_as_new(self) -> bool:
|
|
567
|
+
"""Return true if continue_as_new was called."""
|
|
568
|
+
return self._continue_as_new_flag
|
|
569
|
+
|
|
570
|
+
def create_timer(self, fire_at: datetime.datetime) -> TaskBase:
|
|
571
|
+
"""Create a Timer Task to fire after at the specified deadline.
|
|
572
|
+
|
|
573
|
+
Parameters
|
|
574
|
+
----------
|
|
575
|
+
fire_at : datetime.datetime
|
|
576
|
+
The time for the timer to trigger
|
|
577
|
+
|
|
578
|
+
Returns
|
|
579
|
+
-------
|
|
580
|
+
TaskBase
|
|
581
|
+
A Durable Timer Task that schedules the timer to wake up the activity
|
|
582
|
+
"""
|
|
583
|
+
action = CreateTimerAction(fire_at)
|
|
584
|
+
task = self._generate_task(action, task_constructor=TimerTask)
|
|
585
|
+
return task
|
|
586
|
+
|
|
587
|
+
def wait_for_external_event(self, name: str) -> TaskBase:
|
|
588
|
+
"""Wait asynchronously for an event to be raised with the name `name`.
|
|
589
|
+
|
|
590
|
+
Parameters
|
|
591
|
+
----------
|
|
592
|
+
name : str
|
|
593
|
+
The event name of the event that the task is waiting for.
|
|
594
|
+
|
|
595
|
+
Returns
|
|
596
|
+
-------
|
|
597
|
+
Task
|
|
598
|
+
Task to wait for the event
|
|
599
|
+
"""
|
|
600
|
+
action = WaitForExternalEventAction(name)
|
|
601
|
+
task = self._generate_task(action, id_=name)
|
|
602
|
+
return task
|
|
603
|
+
|
|
604
|
+
def continue_as_new(self, input_: Any):
|
|
605
|
+
"""Schedule the orchestrator to continue as new.
|
|
606
|
+
|
|
607
|
+
Parameters
|
|
608
|
+
----------
|
|
609
|
+
input_ : Any
|
|
610
|
+
The new starting input to the orchestrator.
|
|
611
|
+
"""
|
|
612
|
+
continue_as_new_action: Action = ContinueAsNewAction(input_)
|
|
613
|
+
self._record_fire_and_forget_action(continue_as_new_action)
|
|
614
|
+
self._continue_as_new_flag = True
|
|
615
|
+
|
|
616
|
+
def new_guid(self) -> UUID:
|
|
617
|
+
"""Generate a replay-safe GUID.
|
|
618
|
+
|
|
619
|
+
Returns
|
|
620
|
+
-------
|
|
621
|
+
UUID
|
|
622
|
+
A new globally-unique ID
|
|
623
|
+
"""
|
|
624
|
+
guid_name = f"{self.instance_id}_{self.current_utc_datetime}"\
|
|
625
|
+
f"_{self._new_uuid_counter}"
|
|
626
|
+
self._new_uuid_counter += 1
|
|
627
|
+
guid = uuid5(NAMESPACE_URL, guid_name)
|
|
628
|
+
return guid
|
|
629
|
+
|
|
630
|
+
@property
|
|
631
|
+
def _actions(self) -> List[List[Action]]:
|
|
632
|
+
"""Get the actions payload of this context, for replay in the extension.
|
|
633
|
+
|
|
634
|
+
Returns
|
|
635
|
+
-------
|
|
636
|
+
List[List[Action]]
|
|
637
|
+
The actions of this context
|
|
638
|
+
"""
|
|
639
|
+
if self._replay_schema is ReplaySchema.V1:
|
|
640
|
+
return self._action_payload_v1
|
|
641
|
+
else:
|
|
642
|
+
return [self._action_payload_v2]
|
|
643
|
+
|
|
644
|
+
def _add_to_actions(self, action_repr: Union[List[Action], Action]):
|
|
645
|
+
"""Add a Task's actions payload to the context's actions array.
|
|
646
|
+
|
|
647
|
+
Parameters
|
|
648
|
+
----------
|
|
649
|
+
action_repr : Union[List[Action], Action]
|
|
650
|
+
The tasks to add
|
|
651
|
+
"""
|
|
652
|
+
# Do not add further actions after `continue_as_new` has been
|
|
653
|
+
# called
|
|
654
|
+
if self.will_continue_as_new:
|
|
655
|
+
return
|
|
656
|
+
|
|
657
|
+
if self._replay_schema is ReplaySchema.V1 and isinstance(action_repr, list):
|
|
658
|
+
self._action_payload_v1.append(action_repr)
|
|
659
|
+
elif self._replay_schema is ReplaySchema.V2 and isinstance(action_repr, Action):
|
|
660
|
+
self._action_payload_v2.append(action_repr)
|
|
661
|
+
else:
|
|
662
|
+
raise Exception(f"DF-internal exception: ActionRepr of signature {type(action_repr)}"
|
|
663
|
+
f"is not compatible on ReplaySchema {self._replay_schema.name}. ")
|
|
664
|
+
|
|
665
|
+
def _pretty_print_history(self) -> str:
|
|
666
|
+
"""Get a pretty-printed version of the orchestration's internal history."""
|
|
667
|
+
def history_to_string(event):
|
|
668
|
+
json_dict = {}
|
|
669
|
+
for key, val in inspect.getmembers(event):
|
|
670
|
+
if not key.startswith('_') and not inspect.ismethod(val):
|
|
671
|
+
if isinstance(val, datetime.date):
|
|
672
|
+
val = val.replace(tzinfo=timezone.utc).timetuple()
|
|
673
|
+
json_dict[key] = val
|
|
674
|
+
return json.dumps(json_dict)
|
|
675
|
+
return str(list(map(history_to_string, self._histories)))
|
|
676
|
+
|
|
677
|
+
def _add_to_open_tasks(self, task: TaskBase):
|
|
678
|
+
|
|
679
|
+
if task._is_scheduled:
|
|
680
|
+
return
|
|
681
|
+
|
|
682
|
+
if isinstance(task, AtomicTask):
|
|
683
|
+
if task.id is None:
|
|
684
|
+
task.id = self._sequence_number
|
|
685
|
+
self._sequence_number += 1
|
|
686
|
+
self.open_tasks[task.id] = task
|
|
687
|
+
elif task.id != -1:
|
|
688
|
+
self.open_tasks[task.id].append(task)
|
|
689
|
+
|
|
690
|
+
if task.id in self.deferred_tasks:
|
|
691
|
+
task_update_action = self.deferred_tasks[task.id]
|
|
692
|
+
task_update_action()
|
|
693
|
+
else:
|
|
694
|
+
for child in task.children:
|
|
695
|
+
self._add_to_open_tasks(child)
|
|
696
|
+
|
|
697
|
+
def _get_function_name(self, name: FunctionBuilder,
|
|
698
|
+
trigger_type: Union[OrchestrationTrigger, ActivityTrigger]):
|
|
699
|
+
try:
|
|
700
|
+
if (isinstance(name._function._trigger, trigger_type)):
|
|
701
|
+
name = name._function._name
|
|
702
|
+
return name
|
|
703
|
+
else:
|
|
704
|
+
if (trigger_type == OrchestrationTrigger):
|
|
705
|
+
trigger_type = "OrchestrationTrigger"
|
|
706
|
+
else:
|
|
707
|
+
trigger_type = "ActivityTrigger"
|
|
708
|
+
error_message = "Received function with Trigger-type `"\
|
|
709
|
+
+ name._function._trigger.type\
|
|
710
|
+
+ "` but expected `" + trigger_type + "`. Ensure your "\
|
|
711
|
+
"function is annotated with the `" + trigger_type +\
|
|
712
|
+
"` decorator or directly pass in the name of the "\
|
|
713
|
+
"function as a string."
|
|
714
|
+
raise ValueError(error_message)
|
|
715
|
+
except AttributeError as e:
|
|
716
|
+
e.message = "Durable Functions SDK internal error: an "\
|
|
717
|
+
"expected attribute is missing from the `FunctionBuilder` "\
|
|
718
|
+
"object in the Python V2 programming model. Please report "\
|
|
719
|
+
"this bug in the Durable Functions Python SDK repo: "\
|
|
720
|
+
"https://github.com/Azure/azure-functions-durable-python.\n"\
|
|
721
|
+
"Error trace: " + e.message
|
|
722
|
+
raise e
|