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