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