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,781 +1,812 @@
1
- import json
2
- from datetime import datetime
3
- from typing import List, Any, Optional, Dict, Union
4
- from time import time
5
- from asyncio import sleep
6
- from urllib.parse import urlparse, quote
7
-
8
- import azure.functions as func
9
-
10
- from .PurgeHistoryResult import PurgeHistoryResult
11
- from .DurableOrchestrationStatus import DurableOrchestrationStatus
12
- from .EntityStateResponse import EntityStateResponse
13
- from .RpcManagementOptions import RpcManagementOptions
14
- from .OrchestrationRuntimeStatus import OrchestrationRuntimeStatus
15
- from ..models.DurableOrchestrationBindings import DurableOrchestrationBindings
16
- from .utils.http_utils import get_async_request, post_async_request, delete_async_request
17
- from .utils.entity_utils import EntityId
18
- from azure.functions._durable_functions import _serialize_custom_object
19
-
20
-
21
- class DurableOrchestrationClient:
22
- """Durable Orchestration Client.
23
-
24
- Client for starting, querying, terminating and raising events to
25
- orchestration instances.
26
- """
27
-
28
- def __init__(self, context: str):
29
- self.task_hub_name: str
30
- self._uniqueWebHookOrigins: List[str]
31
- self._event_name_placeholder: str = "{eventName}"
32
- self._function_name_placeholder: str = "{functionName}"
33
- self._instance_id_placeholder: str = "[/{instanceId}]"
34
- self._reason_placeholder: str = "{text}"
35
- self._created_time_from_query_key: str = "createdTimeFrom"
36
- self._created_time_to_query_key: str = "createdTimeTo"
37
- self._runtime_status_query_key: str = "runtimeStatus"
38
- self._show_history_query_key: str = "showHistory"
39
- self._show_history_output_query_key: str = "showHistoryOutput"
40
- self._show_input_query_key: str = "showInput"
41
- self._orchestration_bindings: DurableOrchestrationBindings = \
42
- DurableOrchestrationBindings.from_json(context)
43
- self._post_async_request = post_async_request
44
- self._get_async_request = get_async_request
45
- self._delete_async_request = delete_async_request
46
-
47
- async def start_new(self,
48
- orchestration_function_name: str,
49
- instance_id: Optional[str] = None,
50
- client_input: Optional[Any] = None) -> str:
51
- """Start a new instance of the specified orchestrator function.
52
-
53
- If an orchestration instance with the specified ID already exists, the
54
- existing instance will be silently replaced by this new instance.
55
-
56
- Parameters
57
- ----------
58
- orchestration_function_name : str
59
- The name of the orchestrator function to start.
60
- instance_id : Optional[str]
61
- The ID to use for the new orchestration instance. If no instance id is specified,
62
- the Durable Functions extension will generate a random GUID (recommended).
63
- client_input : Optional[Any]
64
- JSON-serializable input value for the orchestrator function.
65
-
66
- Returns
67
- -------
68
- str
69
- The ID of the new orchestration instance if successful, None if not.
70
- """
71
- request_url = self._get_start_new_url(
72
- instance_id=instance_id, orchestration_function_name=orchestration_function_name)
73
-
74
- response: List[Any] = await self._post_async_request(
75
- request_url, self._get_json_input(client_input))
76
-
77
- status_code: int = response[0]
78
- if status_code <= 202 and response[1]:
79
- return response[1]["id"]
80
- elif status_code == 400:
81
- # Orchestrator not found, report clean exception
82
- exception_data: Dict[str, str] = response[1]
83
- exception_message = exception_data["ExceptionMessage"]
84
- raise Exception(exception_message)
85
- else:
86
- # Catch all: simply surfacing the durable-extension exception
87
- # we surface the stack trace too, since this may be a more involed exception
88
- ex_message: Any = response[1]
89
- raise Exception(ex_message)
90
-
91
- def create_check_status_response(
92
- self, request: func.HttpRequest, instance_id: str) -> func.HttpResponse:
93
- """Create a HttpResponse that contains useful information for \
94
- checking the status of the specified instance.
95
-
96
- Parameters
97
- ----------
98
- request : HttpRequest
99
- The HTTP request that triggered the current orchestration instance.
100
- instance_id : str
101
- The ID of the orchestration instance to check.
102
-
103
- Returns
104
- -------
105
- HttpResponse
106
- An HTTP 202 response with a Location header
107
- and a payload containing instance management URLs
108
- """
109
- http_management_payload = self.get_client_response_links(request, instance_id)
110
- response_args = {
111
- "status_code": 202,
112
- "body": json.dumps(http_management_payload),
113
- "headers": {
114
- "Content-Type": "application/json",
115
- "Location": http_management_payload["statusQueryGetUri"],
116
- "Retry-After": "10",
117
- },
118
- }
119
- return func.HttpResponse(**response_args)
120
-
121
- def create_http_management_payload(self, instance_id: str) -> Dict[str, str]:
122
- """Create a dictionary of orchestrator management urls.
123
-
124
- Parameters
125
- ----------
126
- instance_id : str
127
- The ID of the orchestration instance to check.
128
-
129
- Returns
130
- -------
131
- Dict[str, str]
132
- a dictionary object of orchestrator instance management urls
133
- """
134
- return self.get_client_response_links(None, instance_id)
135
-
136
- async def read_entity_state(
137
- self,
138
- entityId: EntityId,
139
- task_hub_name: Optional[str] = None,
140
- connection_name: Optional[str] = None,
141
- ) -> EntityStateResponse:
142
- """Read the state of the entity.
143
-
144
- Parameters
145
- ----------
146
- entityId : EntityId
147
- The EntityId of the targeted entity.
148
- task_hub_name : Optional[str]
149
- The task hub name of the target entity.
150
- connection_name : Optional[str]
151
- The name of the connection string associated with [task_hub_name].
152
-
153
- Raises
154
- ------
155
- Exception:
156
- When an unexpected status code is returned
157
-
158
- Returns
159
- -------
160
- EntityStateResponse
161
- container object representing the state of the entity
162
- """
163
- options = RpcManagementOptions(
164
- connection_name=connection_name,
165
- task_hub_name=task_hub_name,
166
- entity_Id=entityId,
167
- )
168
-
169
- request_url = options.to_url(self._orchestration_bindings.rpc_base_url)
170
- response = await self._get_async_request(request_url)
171
-
172
- switch_statement = {
173
- 200: lambda: EntityStateResponse(True, response[1]),
174
- 404: lambda: EntityStateResponse(False),
175
- }
176
-
177
- result = switch_statement.get(response[0])
178
-
179
- if not result:
180
- raise Exception(
181
- f"The operation failed with an unexpected status code {response[0]}"
182
- )
183
-
184
- return result()
185
-
186
- def get_client_response_links(
187
- self,
188
- request: Optional[func.HttpRequest], instance_id: str) -> Dict[str, str]:
189
- """Create a dictionary of orchestrator management urls.
190
-
191
- Parameters
192
- ----------
193
- request : Optional[HttpRequest]
194
- The HTTP request that triggered the current orchestration instance.
195
- instance_id : str
196
- The ID of the orchestration instance to check.
197
-
198
- Returns
199
- -------
200
- Dict[str, str]
201
- a dictionary object of orchestrator instance management urls
202
- """
203
- payload = self._orchestration_bindings.management_urls.copy()
204
-
205
- for key, _ in payload.items():
206
- if not (request is None) and request.url:
207
- payload[key] = self._replace_url_origin(request.url, payload[key])
208
- payload[key] = payload[key].replace(
209
- self._orchestration_bindings.management_urls["id"], instance_id)
210
-
211
- return payload
212
-
213
- async def raise_event(
214
- self, instance_id: str, event_name: str, event_data: Any = None,
215
- task_hub_name: str = None, connection_name: str = None) -> None:
216
- """Send an event notification message to a waiting orchestration instance.
217
-
218
- In order to handle the event, the target orchestration instance must be
219
- waiting for an event named `eventName` using waitForExternalEvent API.
220
-
221
- Parameters
222
- ----------
223
- instance_id : str
224
- The ID of the orchestration instance that will handle the event.
225
- event_name : str
226
- The name of the event.
227
- event_data : Any, optional
228
- The JSON-serializable data associated with the event.
229
- task_hub_name : str, optional
230
- The TaskHubName of the orchestration that will handle the event.
231
- connection_name : str, optional
232
- The name of the connection string associated with `taskHubName.`
233
-
234
- Raises
235
- ------
236
- ValueError
237
- event name must be a valid string.
238
- Exception
239
- Raises an exception if the status code is 404 or 400 when raising the event.
240
- """
241
- if event_name == "":
242
- raise ValueError("event_name must be a non-empty string.")
243
-
244
- request_url = self._get_raise_event_url(
245
- instance_id, event_name, task_hub_name, connection_name)
246
-
247
- response = await self._post_async_request(request_url, json.dumps(event_data))
248
-
249
- switch_statement = {
250
- 202: lambda: None,
251
- 410: lambda: f"Instance with ID {instance_id} is gone: either completed or failed",
252
- 404: lambda: f"No instance with ID {instance_id} found.",
253
- 400: lambda: "Only application/json request content is supported"
254
- }
255
- has_error_message = switch_statement.get(
256
- response[0], lambda: f"Webhook returned unrecognized status code {response[0]}")
257
- error_message = has_error_message()
258
- if error_message:
259
- raise Exception(error_message)
260
-
261
- async def get_status(self, instance_id: str, show_history: bool = False,
262
- show_history_output: bool = False,
263
- show_input: bool = False) -> DurableOrchestrationStatus:
264
- """Get the status of the specified orchestration instance.
265
-
266
- Parameters
267
- ----------
268
- instance_id : str
269
- The ID of the orchestration instance to query.
270
- show_history: bool
271
- Boolean marker for including execution history in the response.
272
- show_history_output: bool
273
- Boolean marker for including output in the execution history response.
274
- show_input: bool
275
- Boolean marker for including the input in the response.
276
-
277
- Returns
278
- -------
279
- DurableOrchestrationStatus
280
- The status of the requested orchestration instance
281
- """
282
- options = RpcManagementOptions(instance_id=instance_id, show_history=show_history,
283
- show_history_output=show_history_output,
284
- show_input=show_input)
285
- request_url = options.to_url(self._orchestration_bindings.rpc_base_url)
286
- response = await self._get_async_request(request_url)
287
- switch_statement = {
288
- 200: lambda: None, # instance completed
289
- 202: lambda: None, # instance in progress
290
- 400: lambda: None, # instance failed or terminated
291
- 404: lambda: None, # instance not found or pending
292
- 500: lambda: None # instance failed with unhandled exception
293
- }
294
-
295
- has_error_message = switch_statement.get(
296
- response[0],
297
- lambda: f"The operation failed with an unexpected status code {response[0]}")
298
- error_message = has_error_message()
299
- if error_message:
300
- raise Exception(error_message)
301
- else:
302
- return DurableOrchestrationStatus.from_json(response[1])
303
-
304
- async def get_status_all(self) -> List[DurableOrchestrationStatus]:
305
- """Get the status of all orchestration instances.
306
-
307
- Returns
308
- -------
309
- DurableOrchestrationStatus
310
- The status of the requested orchestration instances
311
- """
312
- options = RpcManagementOptions()
313
- request_url = options.to_url(self._orchestration_bindings.rpc_base_url)
314
- response = await self._get_async_request(request_url)
315
- switch_statement = {
316
- 200: lambda: None, # instance completed
317
- }
318
-
319
- has_error_message = switch_statement.get(
320
- response[0],
321
- lambda: f"The operation failed with an unexpected status code {response[0]}")
322
- error_message = has_error_message()
323
- if error_message:
324
- raise Exception(error_message)
325
- else:
326
- statuses: List[Any] = response[1]
327
- return [DurableOrchestrationStatus.from_json(o) for o in statuses]
328
-
329
- async def get_status_by(self, created_time_from: datetime = None,
330
- created_time_to: datetime = None,
331
- runtime_status: List[OrchestrationRuntimeStatus] = None) \
332
- -> List[DurableOrchestrationStatus]:
333
- """Get the status of all orchestration instances that match the specified conditions.
334
-
335
- Parameters
336
- ----------
337
- created_time_from : datetime
338
- Return orchestration instances which were created after this Date.
339
- created_time_to: datetime
340
- Return orchestration instances which were created before this Date.
341
- runtime_status: List[OrchestrationRuntimeStatus]
342
- Return orchestration instances which match any of the runtimeStatus values
343
- in this list.
344
-
345
- Returns
346
- -------
347
- DurableOrchestrationStatus
348
- The status of the requested orchestration instances
349
- """
350
- # TODO: do we really want folks to us this without specifying all the args?
351
- options = RpcManagementOptions(created_time_from=created_time_from,
352
- created_time_to=created_time_to,
353
- runtime_status=runtime_status)
354
- request_url = options.to_url(self._orchestration_bindings.rpc_base_url)
355
- response = await self._get_async_request(request_url)
356
- switch_statement = {
357
- 200: lambda: None, # instance completed
358
- }
359
-
360
- has_error_message = switch_statement.get(
361
- response[0],
362
- lambda: f"The operation failed with an unexpected status code {response[0]}")
363
- error_message = has_error_message()
364
- if error_message:
365
- raise Exception(error_message)
366
- else:
367
- return [DurableOrchestrationStatus.from_json(o) for o in response[1]]
368
-
369
- async def purge_instance_history(self, instance_id: str) -> PurgeHistoryResult:
370
- """Delete the history of the specified orchestration instance.
371
-
372
- Parameters
373
- ----------
374
- instance_id : str
375
- The ID of the orchestration instance to delete.
376
-
377
- Returns
378
- -------
379
- PurgeHistoryResult
380
- The results of the request to delete the orchestration instance
381
- """
382
- request_url = f"{self._orchestration_bindings.rpc_base_url}instances/{instance_id}"
383
- response = await self._delete_async_request(request_url)
384
- return self._parse_purge_instance_history_response(response)
385
-
386
- async def purge_instance_history_by(
387
- self, created_time_from: Optional[datetime] = None,
388
- created_time_to: Optional[datetime] = None,
389
- runtime_status: Optional[List[OrchestrationRuntimeStatus]] = None) \
390
- -> PurgeHistoryResult:
391
- """Delete the history of all orchestration instances that match the specified conditions.
392
-
393
- Parameters
394
- ----------
395
- created_time_from : Optional[datetime]
396
- Delete orchestration history which were created after this Date.
397
- created_time_to: Optional[datetime]
398
- Delete orchestration history which were created before this Date.
399
- runtime_status: Optional[List[OrchestrationRuntimeStatus]]
400
- Delete orchestration instances which match any of the runtimeStatus values
401
- in this list.
402
-
403
- Returns
404
- -------
405
- PurgeHistoryResult
406
- The results of the request to purge history
407
- """
408
- options = RpcManagementOptions(created_time_from=created_time_from,
409
- created_time_to=created_time_to,
410
- runtime_status=runtime_status)
411
- request_url = options.to_url(self._orchestration_bindings.rpc_base_url)
412
- response = await self._delete_async_request(request_url)
413
- return self._parse_purge_instance_history_response(response)
414
-
415
- async def terminate(self, instance_id: str, reason: str) -> None:
416
- """Terminate the specified orchestration instance.
417
-
418
- Parameters
419
- ----------
420
- instance_id : str
421
- The ID of the orchestration instance to query.
422
- reason: str
423
- The reason for terminating the instance.
424
-
425
- Raises
426
- ------
427
- Exception:
428
- When the terminate call failed with an unexpected status code
429
-
430
- Returns
431
- -------
432
- None
433
- """
434
- request_url = f"{self._orchestration_bindings.rpc_base_url}instances/{instance_id}/" \
435
- f"terminate?reason={quote(reason)}"
436
- response = await self._post_async_request(request_url, None)
437
- switch_statement = {
438
- 202: lambda: None, # instance in progress
439
- 410: lambda: None, # instance failed or terminated
440
- 404: lambda: f"No instance with ID '{instance_id}' found.",
441
- }
442
-
443
- has_error_message = switch_statement.get(
444
- response[0],
445
- lambda: f"The operation failed with an unexpected status code {response[0]}")
446
- error_message = has_error_message()
447
- if error_message:
448
- raise Exception(error_message)
449
-
450
- async def wait_for_completion_or_create_check_status_response(
451
- self, request, instance_id: str, timeout_in_milliseconds: int = 10000,
452
- retry_interval_in_milliseconds: int = 1000) -> func.HttpResponse:
453
- """Create an HTTP response.
454
-
455
- The response either contains a payload of management URLs for a non-completed instance or
456
- contains the payload containing the output of the completed orchestration.
457
-
458
- If the orchestration does not complete within the specified timeout, then the HTTP response
459
- will be identical to that of [[createCheckStatusResponse]].
460
-
461
- Parameters
462
- ----------
463
- request
464
- The HTTP request that triggered the current function.
465
- instance_id:
466
- The unique ID of the instance to check.
467
- timeout_in_milliseconds:
468
- Total allowed timeout for output from the durable function.
469
- The default value is 10 seconds.
470
- retry_interval_in_milliseconds:
471
- The timeout between checks for output from the durable function.
472
- The default value is 1 second.
473
- """
474
- if retry_interval_in_milliseconds > timeout_in_milliseconds:
475
- raise Exception(f'Total timeout {timeout_in_milliseconds} (ms) should be bigger than '
476
- f'retry timeout {retry_interval_in_milliseconds} (ms)')
477
-
478
- checking = True
479
- start_time = time()
480
-
481
- while checking:
482
- status = await self.get_status(instance_id)
483
-
484
- if status:
485
- switch_statement = {
486
- OrchestrationRuntimeStatus.Completed:
487
- lambda: self._create_http_response(200, status.output),
488
- OrchestrationRuntimeStatus.Canceled:
489
- lambda: self._create_http_response(200, status.to_json()),
490
- OrchestrationRuntimeStatus.Terminated:
491
- lambda: self._create_http_response(200, status.to_json()),
492
- OrchestrationRuntimeStatus.Failed:
493
- lambda: self._create_http_response(500, status.to_json()),
494
- None:
495
- None
496
- }
497
-
498
- result = switch_statement.get(status.runtime_status)
499
- if result:
500
- return result()
501
-
502
- elapsed = time() - start_time
503
- elapsed_in_milliseconds = elapsed * 1000
504
- if elapsed_in_milliseconds < timeout_in_milliseconds:
505
- remaining_time = timeout_in_milliseconds - elapsed_in_milliseconds
506
- sleep_time = retry_interval_in_milliseconds \
507
- if remaining_time > retry_interval_in_milliseconds else remaining_time
508
- sleep_time /= 1000
509
- await sleep(sleep_time)
510
- else:
511
- return self.create_check_status_response(request, instance_id)
512
- return self.create_check_status_response(request, instance_id)
513
-
514
- async def signal_entity(self, entityId: EntityId, operation_name: str,
515
- operation_input: Optional[Any] = None,
516
- task_hub_name: Optional[str] = None,
517
- connection_name: Optional[str] = None) -> None:
518
- """Signals an entity to perform an operation.
519
-
520
- Parameters
521
- ----------
522
- entityId : EntityId
523
- The EntityId of the targeted entity to perform operation.
524
- operation_name: str
525
- The name of the operation.
526
- operation_input: Optional[Any]
527
- The content for the operation.
528
- task_hub_name: Optional[str]
529
- The task hub name of the target entity.
530
- connection_name: Optional[str]
531
- The name of the connection string associated with [task_hub_name].
532
-
533
- Raises
534
- ------
535
- Exception:
536
- When the signal entity call failed with an unexpected status code
537
-
538
- Returns
539
- -------
540
- None
541
- """
542
- options = RpcManagementOptions(operation_name=operation_name,
543
- connection_name=connection_name,
544
- task_hub_name=task_hub_name,
545
- entity_Id=entityId)
546
-
547
- request_url = options.to_url(self._orchestration_bindings.rpc_base_url)
548
- response = await self._post_async_request(
549
- request_url,
550
- json.dumps(operation_input) if operation_input else None)
551
-
552
- switch_statement = {
553
- 202: lambda: None # signal accepted
554
- }
555
-
556
- has_error_message = switch_statement.get(
557
- response[0],
558
- lambda: f"The operation failed with an unexpected status code {response[0]}")
559
-
560
- error_message = has_error_message()
561
-
562
- if error_message:
563
- raise Exception(error_message)
564
-
565
- @staticmethod
566
- def _create_http_response(
567
- status_code: int, body: Union[str, Any]) -> func.HttpResponse:
568
- body_as_json = body if isinstance(body, str) else json.dumps(body)
569
- response_args = {
570
- "status_code": status_code,
571
- "body": body_as_json,
572
- "mimetype": "application/json",
573
- "headers": {
574
- "Content-Type": "application/json",
575
- }
576
- }
577
- return func.HttpResponse(**response_args)
578
-
579
- @staticmethod
580
- def _get_json_input(client_input: object) -> Optional[str]:
581
- """Serialize the orchestrator input.
582
-
583
- Parameters
584
- ----------
585
- client_input: object
586
- The client's input, which we need to serialize
587
-
588
- Returns
589
- -------
590
- Optional[str]
591
- If `client_input` is not None, return a string representing
592
- the JSON-serialization of `client_input`. Otherwise, returns
593
- None
594
-
595
- Exceptions
596
- ----------
597
- TypeError
598
- If the JSON serialization failed, see `serialize_custom_object`
599
- """
600
- if client_input is not None:
601
- return json.dumps(client_input, default=_serialize_custom_object)
602
- return None
603
-
604
- @staticmethod
605
- def _replace_url_origin(request_url: str, value_url: str) -> str:
606
- request_parsed_url = urlparse(request_url)
607
- value_parsed_url = urlparse(value_url)
608
- request_url_origin = '{url.scheme}://{url.netloc}/'.format(url=request_parsed_url)
609
- value_url_origin = '{url.scheme}://{url.netloc}/'.format(url=value_parsed_url)
610
- value_url = value_url.replace(value_url_origin, request_url_origin)
611
- return value_url
612
-
613
- @staticmethod
614
- def _parse_purge_instance_history_response(
615
- response: List[Any]) -> PurgeHistoryResult:
616
- switch_statement = {
617
- 200: lambda: PurgeHistoryResult.from_json(response[1]), # instance completed
618
- 404: lambda: PurgeHistoryResult(instancesDeleted=0), # instance not found
619
- }
620
-
621
- switch_result = switch_statement.get(
622
- response[0],
623
- lambda: f"The operation failed with an unexpected status code {response[0]}")
624
- result = switch_result()
625
- if isinstance(result, PurgeHistoryResult):
626
- return result
627
- else:
628
- raise Exception(result)
629
-
630
- def _get_start_new_url(
631
- self, instance_id: Optional[str], orchestration_function_name: str) -> str:
632
- instance_path = f'/{instance_id}' if instance_id is not None else ''
633
- request_url = f'{self._orchestration_bindings.rpc_base_url}orchestrators/' \
634
- f'{orchestration_function_name}{instance_path}'
635
- return request_url
636
-
637
- def _get_raise_event_url(
638
- self, instance_id: str, event_name: str,
639
- task_hub_name: Optional[str], connection_name: Optional[str]) -> str:
640
- request_url = f'{self._orchestration_bindings.rpc_base_url}' \
641
- f'instances/{instance_id}/raiseEvent/{event_name}'
642
-
643
- query: List[str] = []
644
- if task_hub_name:
645
- query.append(f'taskHub={task_hub_name}')
646
-
647
- if connection_name:
648
- query.append(f'connection={connection_name}')
649
-
650
- if len(query) > 0:
651
- request_url += "?" + "&".join(query)
652
-
653
- return request_url
654
-
655
- async def rewind(self,
656
- instance_id: str,
657
- reason: str,
658
- task_hub_name: Optional[str] = None,
659
- connection_name: Optional[str] = None):
660
- """Return / "rewind" a failed orchestration instance to a prior "healthy" state.
661
-
662
- Parameters
663
- ----------
664
- instance_id: str
665
- The ID of the orchestration instance to rewind.
666
- reason: str
667
- The reason for rewinding the orchestration instance.
668
- task_hub_name: Optional[str]
669
- The TaskHub of the orchestration to rewind
670
- connection_name: Optional[str]
671
- Name of the application setting containing the storage
672
- connection string to use.
673
-
674
- Raises
675
- ------
676
- Exception:
677
- In case of a failure, it reports the reason for the exception
678
- """
679
- request_url: str = ""
680
- if self._orchestration_bindings.rpc_base_url:
681
- path = f"instances/{instance_id}/rewind?reason={reason}"
682
- query: List[str] = []
683
- if not (task_hub_name is None):
684
- query.append(f"taskHub={task_hub_name}")
685
- if not (connection_name is None):
686
- query.append(f"connection={connection_name}")
687
- if len(query) > 0:
688
- path += "&" + "&".join(query)
689
-
690
- request_url = f"{self._orchestration_bindings.rpc_base_url}" + path
691
- else:
692
- raise Exception("The Python SDK only supports RPC endpoints."
693
- + "Please remove the `localRpcEnabled` setting from host.json")
694
-
695
- response = await self._post_async_request(request_url, None)
696
- status: int = response[0]
697
- ex_msg: str = ""
698
- if status == 200 or status == 202:
699
- return
700
- elif status == 404:
701
- ex_msg = f"No instance with ID {instance_id} found."
702
- raise Exception(ex_msg)
703
- elif status == 410:
704
- ex_msg = "The rewind operation is only supported on failed orchestration instances."
705
- raise Exception(ex_msg)
706
- elif isinstance(response[1], str):
707
- ex_msg = response[1]
708
- raise Exception(ex_msg)
709
- else:
710
- ex_msg = "Received unexpected payload from the durable-extension: " + str(response)
711
- raise Exception(ex_msg)
712
-
713
- async def suspend(self, instance_id: str, reason: str) -> None:
714
- """Suspend the specified orchestration instance.
715
-
716
- Parameters
717
- ----------
718
- instance_id : str
719
- The ID of the orchestration instance to suspend.
720
- reason: str
721
- The reason for suspending the instance.
722
-
723
- Raises
724
- ------
725
- Exception:
726
- When the suspend call failed with an unexpected status code
727
-
728
- Returns
729
- -------
730
- None
731
- """
732
- request_url = f"{self._orchestration_bindings.rpc_base_url}instances/{instance_id}/" \
733
- f"suspend?reason={quote(reason)}"
734
- response = await self._post_async_request(request_url, None)
735
- switch_statement = {
736
- 202: lambda: None, # instance is suspended
737
- 410: lambda: None, # instance completed
738
- 404: lambda: f"No instance with ID '{instance_id}' found.",
739
- }
740
-
741
- has_error_message = switch_statement.get(
742
- response[0],
743
- lambda: f"The operation failed with an unexpected status code {response[0]}")
744
- error_message = has_error_message()
745
- if error_message:
746
- raise Exception(error_message)
747
-
748
- async def resume(self, instance_id: str, reason: str) -> None:
749
- """Resume the specified orchestration instance.
750
-
751
- Parameters
752
- ----------
753
- instance_id : str
754
- The ID of the orchestration instance to query.
755
- reason: str
756
- The reason for resuming the instance.
757
-
758
- Raises
759
- ------
760
- Exception:
761
- When the resume call failed with an unexpected status code
762
-
763
- Returns
764
- -------
765
- None
766
- """
767
- request_url = f"{self._orchestration_bindings.rpc_base_url}instances/{instance_id}/" \
768
- f"resume?reason={quote(reason)}"
769
- response = await self._post_async_request(request_url, None)
770
- switch_statement = {
771
- 202: lambda: None, # instance is resumed
772
- 410: lambda: None, # instance completed
773
- 404: lambda: f"No instance with ID '{instance_id}' found.",
774
- }
775
-
776
- has_error_message = switch_statement.get(
777
- response[0],
778
- lambda: f"The operation failed with an unexpected status code {response[0]}")
779
- error_message = has_error_message()
780
- if error_message:
781
- raise Exception(error_message)
1
+ import json
2
+ from datetime import datetime
3
+ from typing import List, Any, Optional, Dict, Union
4
+ from time import time
5
+ from asyncio import sleep
6
+ from urllib.parse import urlparse, quote
7
+ from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
8
+
9
+ import azure.functions as func
10
+
11
+ from .PurgeHistoryResult import PurgeHistoryResult
12
+ from .DurableOrchestrationStatus import DurableOrchestrationStatus
13
+ from .EntityStateResponse import EntityStateResponse
14
+ from .RpcManagementOptions import RpcManagementOptions
15
+ from .OrchestrationRuntimeStatus import OrchestrationRuntimeStatus
16
+ from ..models.DurableOrchestrationBindings import DurableOrchestrationBindings
17
+ from .utils.http_utils import get_async_request, post_async_request, delete_async_request
18
+ from .utils.entity_utils import EntityId
19
+ from azure.functions._durable_functions import _serialize_custom_object
20
+
21
+
22
+ class DurableOrchestrationClient:
23
+ """Durable Orchestration Client.
24
+
25
+ Client for starting, querying, terminating and raising events to
26
+ orchestration instances.
27
+ """
28
+
29
+ def __init__(self, context: str):
30
+ self.task_hub_name: str
31
+ self._uniqueWebHookOrigins: List[str]
32
+ self._event_name_placeholder: str = "{eventName}"
33
+ self._function_name_placeholder: str = "{functionName}"
34
+ self._instance_id_placeholder: str = "[/{instanceId}]"
35
+ self._reason_placeholder: str = "{text}"
36
+ self._created_time_from_query_key: str = "createdTimeFrom"
37
+ self._created_time_to_query_key: str = "createdTimeTo"
38
+ self._runtime_status_query_key: str = "runtimeStatus"
39
+ self._show_history_query_key: str = "showHistory"
40
+ self._show_history_output_query_key: str = "showHistoryOutput"
41
+ self._show_input_query_key: str = "showInput"
42
+ self._orchestration_bindings: DurableOrchestrationBindings = \
43
+ DurableOrchestrationBindings.from_json(context)
44
+ self._post_async_request = post_async_request
45
+ self._get_async_request = get_async_request
46
+ self._delete_async_request = delete_async_request
47
+
48
+ async def start_new(self,
49
+ orchestration_function_name: str,
50
+ instance_id: Optional[str] = None,
51
+ client_input: Optional[Any] = None) -> str:
52
+ """Start a new instance of the specified orchestrator function.
53
+
54
+ If an orchestration instance with the specified ID already exists, the
55
+ existing instance will be silently replaced by this new instance.
56
+
57
+ Parameters
58
+ ----------
59
+ orchestration_function_name : str
60
+ The name of the orchestrator function to start.
61
+ instance_id : Optional[str]
62
+ The ID to use for the new orchestration instance. If no instance id is specified,
63
+ the Durable Functions extension will generate a random GUID (recommended).
64
+ client_input : Optional[Any]
65
+ JSON-serializable input value for the orchestrator function.
66
+
67
+ Returns
68
+ -------
69
+ str
70
+ The ID of the new orchestration instance if successful, None if not.
71
+ """
72
+ request_url = self._get_start_new_url(
73
+ instance_id=instance_id, orchestration_function_name=orchestration_function_name)
74
+
75
+ trace_parent, trace_state = DurableOrchestrationClient._get_current_activity_context()
76
+
77
+ response: List[Any] = await self._post_async_request(
78
+ request_url,
79
+ self._get_json_input(client_input),
80
+ trace_parent,
81
+ trace_state)
82
+
83
+ status_code: int = response[0]
84
+ if status_code <= 202 and response[1]:
85
+ return response[1]["id"]
86
+ elif status_code == 400:
87
+ # Orchestrator not found, report clean exception
88
+ exception_data: Dict[str, str] = response[1]
89
+ exception_message = exception_data["ExceptionMessage"]
90
+ raise Exception(exception_message)
91
+ else:
92
+ # Catch all: simply surfacing the durable-extension exception
93
+ # we surface the stack trace too, since this may be a more involed exception
94
+ ex_message: Any = response[1]
95
+ raise Exception(ex_message)
96
+
97
+ def create_check_status_response(
98
+ self, request: func.HttpRequest, instance_id: str) -> func.HttpResponse:
99
+ """Create a HttpResponse that contains useful information for \
100
+ checking the status of the specified instance.
101
+
102
+ Parameters
103
+ ----------
104
+ request : HttpRequest
105
+ The HTTP request that triggered the current orchestration instance.
106
+ instance_id : str
107
+ The ID of the orchestration instance to check.
108
+
109
+ Returns
110
+ -------
111
+ HttpResponse
112
+ An HTTP 202 response with a Location header
113
+ and a payload containing instance management URLs
114
+ """
115
+ http_management_payload = self.get_client_response_links(request, instance_id)
116
+ response_args = {
117
+ "status_code": 202,
118
+ "body": json.dumps(http_management_payload),
119
+ "headers": {
120
+ "Content-Type": "application/json",
121
+ "Location": http_management_payload["statusQueryGetUri"],
122
+ "Retry-After": "10",
123
+ },
124
+ }
125
+ return func.HttpResponse(**response_args)
126
+
127
+ def create_http_management_payload(self, instance_id: str) -> Dict[str, str]:
128
+ """Create a dictionary of orchestrator management urls.
129
+
130
+ Parameters
131
+ ----------
132
+ instance_id : str
133
+ The ID of the orchestration instance to check.
134
+
135
+ Returns
136
+ -------
137
+ Dict[str, str]
138
+ a dictionary object of orchestrator instance management urls
139
+ """
140
+ return self.get_client_response_links(None, instance_id)
141
+
142
+ async def read_entity_state(
143
+ self,
144
+ entityId: EntityId,
145
+ task_hub_name: Optional[str] = None,
146
+ connection_name: Optional[str] = None,
147
+ ) -> EntityStateResponse:
148
+ """Read the state of the entity.
149
+
150
+ Parameters
151
+ ----------
152
+ entityId : EntityId
153
+ The EntityId of the targeted entity.
154
+ task_hub_name : Optional[str]
155
+ The task hub name of the target entity.
156
+ connection_name : Optional[str]
157
+ The name of the connection string associated with [task_hub_name].
158
+
159
+ Raises
160
+ ------
161
+ Exception:
162
+ When an unexpected status code is returned
163
+
164
+ Returns
165
+ -------
166
+ EntityStateResponse
167
+ container object representing the state of the entity
168
+ """
169
+ options = RpcManagementOptions(
170
+ connection_name=connection_name,
171
+ task_hub_name=task_hub_name,
172
+ entity_Id=entityId,
173
+ )
174
+
175
+ request_url = options.to_url(self._orchestration_bindings.rpc_base_url)
176
+ response = await self._get_async_request(request_url)
177
+
178
+ switch_statement = {
179
+ 200: lambda: EntityStateResponse(True, response[1]),
180
+ 404: lambda: EntityStateResponse(False),
181
+ }
182
+
183
+ result = switch_statement.get(response[0])
184
+
185
+ if not result:
186
+ raise Exception(
187
+ f"The operation failed with an unexpected status code {response[0]}"
188
+ )
189
+
190
+ return result()
191
+
192
+ def get_client_response_links(
193
+ self,
194
+ request: Optional[func.HttpRequest], instance_id: str) -> Dict[str, str]:
195
+ """Create a dictionary of orchestrator management urls.
196
+
197
+ Parameters
198
+ ----------
199
+ request : Optional[HttpRequest]
200
+ The HTTP request that triggered the current orchestration instance.
201
+ instance_id : str
202
+ The ID of the orchestration instance to check.
203
+
204
+ Returns
205
+ -------
206
+ Dict[str, str]
207
+ a dictionary object of orchestrator instance management urls
208
+ """
209
+ payload = self._orchestration_bindings.management_urls.copy()
210
+
211
+ for key, _ in payload.items():
212
+ if not (request is None) and request.url:
213
+ payload[key] = self._replace_url_origin(request.url, payload[key])
214
+ payload[key] = payload[key].replace(
215
+ self._orchestration_bindings.management_urls["id"], instance_id)
216
+
217
+ return payload
218
+
219
+ async def raise_event(
220
+ self, instance_id: str, event_name: str, event_data: Any = None,
221
+ task_hub_name: str = None, connection_name: str = None) -> None:
222
+ """Send an event notification message to a waiting orchestration instance.
223
+
224
+ In order to handle the event, the target orchestration instance must be
225
+ waiting for an event named `eventName` using waitForExternalEvent API.
226
+
227
+ Parameters
228
+ ----------
229
+ instance_id : str
230
+ The ID of the orchestration instance that will handle the event.
231
+ event_name : str
232
+ The name of the event.
233
+ event_data : Any, optional
234
+ The JSON-serializable data associated with the event.
235
+ task_hub_name : str, optional
236
+ The TaskHubName of the orchestration that will handle the event.
237
+ connection_name : str, optional
238
+ The name of the connection string associated with `taskHubName.`
239
+
240
+ Raises
241
+ ------
242
+ ValueError
243
+ event name must be a valid string.
244
+ Exception
245
+ Raises an exception if the status code is 404 or 400 when raising the event.
246
+ """
247
+ if event_name == "":
248
+ raise ValueError("event_name must be a non-empty string.")
249
+
250
+ request_url = self._get_raise_event_url(
251
+ instance_id, event_name, task_hub_name, connection_name)
252
+
253
+ response = await self._post_async_request(request_url, json.dumps(event_data))
254
+
255
+ switch_statement = {
256
+ 202: lambda: None,
257
+ 410: lambda: f"Instance with ID {instance_id} is gone: either completed or failed",
258
+ 404: lambda: f"No instance with ID {instance_id} found.",
259
+ 400: lambda: "Only application/json request content is supported"
260
+ }
261
+ has_error_message = switch_statement.get(
262
+ response[0], lambda: f"Webhook returned unrecognized status code {response[0]}")
263
+ error_message = has_error_message()
264
+ if error_message:
265
+ raise Exception(error_message)
266
+
267
+ async def get_status(self, instance_id: str, show_history: bool = False,
268
+ show_history_output: bool = False,
269
+ show_input: bool = False) -> DurableOrchestrationStatus:
270
+ """Get the status of the specified orchestration instance.
271
+
272
+ Parameters
273
+ ----------
274
+ instance_id : str
275
+ The ID of the orchestration instance to query.
276
+ show_history: bool
277
+ Boolean marker for including execution history in the response.
278
+ show_history_output: bool
279
+ Boolean marker for including output in the execution history response.
280
+ show_input: bool
281
+ Boolean marker for including the input in the response.
282
+
283
+ Returns
284
+ -------
285
+ DurableOrchestrationStatus
286
+ The status of the requested orchestration instance
287
+ """
288
+ options = RpcManagementOptions(instance_id=instance_id, show_history=show_history,
289
+ show_history_output=show_history_output,
290
+ show_input=show_input)
291
+ request_url = options.to_url(self._orchestration_bindings.rpc_base_url)
292
+ response = await self._get_async_request(request_url)
293
+ switch_statement = {
294
+ 200: lambda: None, # instance completed
295
+ 202: lambda: None, # instance in progress
296
+ 400: lambda: None, # instance failed or terminated
297
+ 404: lambda: None, # instance not found or pending
298
+ 500: lambda: None # instance failed with unhandled exception
299
+ }
300
+
301
+ has_error_message = switch_statement.get(
302
+ response[0],
303
+ lambda: f"The operation failed with an unexpected status code {response[0]}")
304
+ error_message = has_error_message()
305
+ if error_message:
306
+ raise Exception(error_message)
307
+ else:
308
+ return DurableOrchestrationStatus.from_json(response[1])
309
+
310
+ async def get_status_all(self) -> List[DurableOrchestrationStatus]:
311
+ """Get the status of all orchestration instances.
312
+
313
+ Returns
314
+ -------
315
+ DurableOrchestrationStatus
316
+ The status of the requested orchestration instances
317
+ """
318
+ options = RpcManagementOptions()
319
+ request_url = options.to_url(self._orchestration_bindings.rpc_base_url)
320
+ response = await self._get_async_request(request_url)
321
+ switch_statement = {
322
+ 200: lambda: None, # instance completed
323
+ }
324
+
325
+ has_error_message = switch_statement.get(
326
+ response[0],
327
+ lambda: f"The operation failed with an unexpected status code {response[0]}")
328
+ error_message = has_error_message()
329
+ if error_message:
330
+ raise Exception(error_message)
331
+ else:
332
+ statuses: List[Any] = response[1]
333
+ return [DurableOrchestrationStatus.from_json(o) for o in statuses]
334
+
335
+ async def get_status_by(self, created_time_from: datetime = None,
336
+ created_time_to: datetime = None,
337
+ runtime_status: List[OrchestrationRuntimeStatus] = None) \
338
+ -> List[DurableOrchestrationStatus]:
339
+ """Get the status of all orchestration instances that match the specified conditions.
340
+
341
+ Parameters
342
+ ----------
343
+ created_time_from : datetime
344
+ Return orchestration instances which were created after this Date.
345
+ created_time_to: datetime
346
+ Return orchestration instances which were created before this Date.
347
+ runtime_status: List[OrchestrationRuntimeStatus]
348
+ Return orchestration instances which match any of the runtimeStatus values
349
+ in this list.
350
+
351
+ Returns
352
+ -------
353
+ DurableOrchestrationStatus
354
+ The status of the requested orchestration instances
355
+ """
356
+ # TODO: do we really want folks to us this without specifying all the args?
357
+ options = RpcManagementOptions(created_time_from=created_time_from,
358
+ created_time_to=created_time_to,
359
+ runtime_status=runtime_status)
360
+ request_url = options.to_url(self._orchestration_bindings.rpc_base_url)
361
+ response = await self._get_async_request(request_url)
362
+ switch_statement = {
363
+ 200: lambda: None, # instance completed
364
+ }
365
+
366
+ has_error_message = switch_statement.get(
367
+ response[0],
368
+ lambda: f"The operation failed with an unexpected status code {response[0]}")
369
+ error_message = has_error_message()
370
+ if error_message:
371
+ raise Exception(error_message)
372
+ else:
373
+ return [DurableOrchestrationStatus.from_json(o) for o in response[1]]
374
+
375
+ async def purge_instance_history(self, instance_id: str) -> PurgeHistoryResult:
376
+ """Delete the history of the specified orchestration instance.
377
+
378
+ Parameters
379
+ ----------
380
+ instance_id : str
381
+ The ID of the orchestration instance to delete.
382
+
383
+ Returns
384
+ -------
385
+ PurgeHistoryResult
386
+ The results of the request to delete the orchestration instance
387
+ """
388
+ request_url = f"{self._orchestration_bindings.rpc_base_url}instances/{instance_id}"
389
+ response = await self._delete_async_request(request_url)
390
+ return self._parse_purge_instance_history_response(response)
391
+
392
+ async def purge_instance_history_by(
393
+ self, created_time_from: Optional[datetime] = None,
394
+ created_time_to: Optional[datetime] = None,
395
+ runtime_status: Optional[List[OrchestrationRuntimeStatus]] = None) \
396
+ -> PurgeHistoryResult:
397
+ """Delete the history of all orchestration instances that match the specified conditions.
398
+
399
+ Parameters
400
+ ----------
401
+ created_time_from : Optional[datetime]
402
+ Delete orchestration history which were created after this Date.
403
+ created_time_to: Optional[datetime]
404
+ Delete orchestration history which were created before this Date.
405
+ runtime_status: Optional[List[OrchestrationRuntimeStatus]]
406
+ Delete orchestration instances which match any of the runtimeStatus values
407
+ in this list.
408
+
409
+ Returns
410
+ -------
411
+ PurgeHistoryResult
412
+ The results of the request to purge history
413
+ """
414
+ options = RpcManagementOptions(created_time_from=created_time_from,
415
+ created_time_to=created_time_to,
416
+ runtime_status=runtime_status)
417
+ request_url = options.to_url(self._orchestration_bindings.rpc_base_url)
418
+ response = await self._delete_async_request(request_url)
419
+ return self._parse_purge_instance_history_response(response)
420
+
421
+ async def terminate(self, instance_id: str, reason: str) -> None:
422
+ """Terminate the specified orchestration instance.
423
+
424
+ Parameters
425
+ ----------
426
+ instance_id : str
427
+ The ID of the orchestration instance to query.
428
+ reason: str
429
+ The reason for terminating the instance.
430
+
431
+ Raises
432
+ ------
433
+ Exception:
434
+ When the terminate call failed with an unexpected status code
435
+
436
+ Returns
437
+ -------
438
+ None
439
+ """
440
+ request_url = f"{self._orchestration_bindings.rpc_base_url}instances/{instance_id}/" \
441
+ f"terminate?reason={quote(reason)}"
442
+ response = await self._post_async_request(request_url, None)
443
+ switch_statement = {
444
+ 202: lambda: None, # instance in progress
445
+ 410: lambda: None, # instance failed or terminated
446
+ 404: lambda: f"No instance with ID '{instance_id}' found.",
447
+ }
448
+
449
+ has_error_message = switch_statement.get(
450
+ response[0],
451
+ lambda: f"The operation failed with an unexpected status code {response[0]}")
452
+ error_message = has_error_message()
453
+ if error_message:
454
+ raise Exception(error_message)
455
+
456
+ async def wait_for_completion_or_create_check_status_response(
457
+ self, request, instance_id: str, timeout_in_milliseconds: int = 10000,
458
+ retry_interval_in_milliseconds: int = 1000) -> func.HttpResponse:
459
+ """Create an HTTP response.
460
+
461
+ The response either contains a payload of management URLs for a non-completed instance or
462
+ contains the payload containing the output of the completed orchestration.
463
+
464
+ If the orchestration does not complete within the specified timeout, then the HTTP response
465
+ will be identical to that of [[createCheckStatusResponse]].
466
+
467
+ Parameters
468
+ ----------
469
+ request
470
+ The HTTP request that triggered the current function.
471
+ instance_id:
472
+ The unique ID of the instance to check.
473
+ timeout_in_milliseconds:
474
+ Total allowed timeout for output from the durable function.
475
+ The default value is 10 seconds.
476
+ retry_interval_in_milliseconds:
477
+ The timeout between checks for output from the durable function.
478
+ The default value is 1 second.
479
+ """
480
+ if retry_interval_in_milliseconds > timeout_in_milliseconds:
481
+ raise Exception(f'Total timeout {timeout_in_milliseconds} (ms) should be bigger than '
482
+ f'retry timeout {retry_interval_in_milliseconds} (ms)')
483
+
484
+ checking = True
485
+ start_time = time()
486
+
487
+ while checking:
488
+ status = await self.get_status(instance_id)
489
+
490
+ if status:
491
+ switch_statement = {
492
+ OrchestrationRuntimeStatus.Completed:
493
+ lambda: self._create_http_response(200, status.output),
494
+ OrchestrationRuntimeStatus.Canceled:
495
+ lambda: self._create_http_response(200, status.to_json()),
496
+ OrchestrationRuntimeStatus.Terminated:
497
+ lambda: self._create_http_response(200, status.to_json()),
498
+ OrchestrationRuntimeStatus.Failed:
499
+ lambda: self._create_http_response(500, status.to_json()),
500
+ None:
501
+ None
502
+ }
503
+
504
+ result = switch_statement.get(status.runtime_status)
505
+ if result:
506
+ return result()
507
+
508
+ elapsed = time() - start_time
509
+ elapsed_in_milliseconds = elapsed * 1000
510
+ if elapsed_in_milliseconds < timeout_in_milliseconds:
511
+ remaining_time = timeout_in_milliseconds - elapsed_in_milliseconds
512
+ sleep_time = retry_interval_in_milliseconds \
513
+ if remaining_time > retry_interval_in_milliseconds else remaining_time
514
+ sleep_time /= 1000
515
+ await sleep(sleep_time)
516
+ else:
517
+ return self.create_check_status_response(request, instance_id)
518
+ return self.create_check_status_response(request, instance_id)
519
+
520
+ async def signal_entity(self, entityId: EntityId, operation_name: str,
521
+ operation_input: Optional[Any] = None,
522
+ task_hub_name: Optional[str] = None,
523
+ connection_name: Optional[str] = None) -> None:
524
+ """Signals an entity to perform an operation.
525
+
526
+ Parameters
527
+ ----------
528
+ entityId : EntityId
529
+ The EntityId of the targeted entity to perform operation.
530
+ operation_name: str
531
+ The name of the operation.
532
+ operation_input: Optional[Any]
533
+ The content for the operation.
534
+ task_hub_name: Optional[str]
535
+ The task hub name of the target entity.
536
+ connection_name: Optional[str]
537
+ The name of the connection string associated with [task_hub_name].
538
+
539
+ Raises
540
+ ------
541
+ Exception:
542
+ When the signal entity call failed with an unexpected status code
543
+
544
+ Returns
545
+ -------
546
+ None
547
+ """
548
+ options = RpcManagementOptions(operation_name=operation_name,
549
+ connection_name=connection_name,
550
+ task_hub_name=task_hub_name,
551
+ entity_Id=entityId)
552
+
553
+ request_url = options.to_url(self._orchestration_bindings.rpc_base_url)
554
+
555
+ trace_parent, trace_state = DurableOrchestrationClient._get_current_activity_context()
556
+
557
+ response = await self._post_async_request(
558
+ request_url,
559
+ json.dumps(operation_input) if operation_input else None,
560
+ trace_parent,
561
+ trace_state)
562
+
563
+ switch_statement = {
564
+ 202: lambda: None # signal accepted
565
+ }
566
+
567
+ has_error_message = switch_statement.get(
568
+ response[0],
569
+ lambda: f"The operation failed with an unexpected status code {response[0]}")
570
+
571
+ error_message = has_error_message()
572
+
573
+ if error_message:
574
+ raise Exception(error_message)
575
+
576
+ @staticmethod
577
+ def _create_http_response(
578
+ status_code: int, body: Union[str, Any]) -> func.HttpResponse:
579
+ body_as_json = body if isinstance(body, str) else json.dumps(body)
580
+ response_args = {
581
+ "status_code": status_code,
582
+ "body": body_as_json,
583
+ "mimetype": "application/json",
584
+ "headers": {
585
+ "Content-Type": "application/json",
586
+ }
587
+ }
588
+ return func.HttpResponse(**response_args)
589
+
590
+ @staticmethod
591
+ def _get_json_input(client_input: object) -> Optional[str]:
592
+ """Serialize the orchestrator input.
593
+
594
+ Parameters
595
+ ----------
596
+ client_input: object
597
+ The client's input, which we need to serialize
598
+
599
+ Returns
600
+ -------
601
+ Optional[str]
602
+ If `client_input` is not None, return a string representing
603
+ the JSON-serialization of `client_input`. Otherwise, returns
604
+ None
605
+
606
+ Exceptions
607
+ ----------
608
+ TypeError
609
+ If the JSON serialization failed, see `serialize_custom_object`
610
+ """
611
+ if client_input is not None:
612
+ return json.dumps(client_input, default=_serialize_custom_object)
613
+ return None
614
+
615
+ @staticmethod
616
+ def _replace_url_origin(request_url: str, value_url: str) -> str:
617
+ request_parsed_url = urlparse(request_url)
618
+ value_parsed_url = urlparse(value_url)
619
+ request_url_origin = '{url.scheme}://{url.netloc}/'.format(url=request_parsed_url)
620
+ value_url_origin = '{url.scheme}://{url.netloc}/'.format(url=value_parsed_url)
621
+ value_url = value_url.replace(value_url_origin, request_url_origin)
622
+ return value_url
623
+
624
+ @staticmethod
625
+ def _parse_purge_instance_history_response(
626
+ response: List[Any]) -> PurgeHistoryResult:
627
+ switch_statement = {
628
+ 200: lambda: PurgeHistoryResult.from_json(response[1]), # instance completed
629
+ 404: lambda: PurgeHistoryResult(instancesDeleted=0), # instance not found
630
+ }
631
+
632
+ switch_result = switch_statement.get(
633
+ response[0],
634
+ lambda: f"The operation failed with an unexpected status code {response[0]}")
635
+ result = switch_result()
636
+ if isinstance(result, PurgeHistoryResult):
637
+ return result
638
+ else:
639
+ raise Exception(result)
640
+
641
+ def _get_start_new_url(
642
+ self, instance_id: Optional[str], orchestration_function_name: str) -> str:
643
+ instance_path = f'/{instance_id}' if instance_id is not None else ''
644
+ request_url = f'{self._orchestration_bindings.rpc_base_url}orchestrators/' \
645
+ f'{orchestration_function_name}{instance_path}'
646
+ return request_url
647
+
648
+ def _get_raise_event_url(
649
+ self, instance_id: str, event_name: str,
650
+ task_hub_name: Optional[str], connection_name: Optional[str]) -> str:
651
+ request_url = f'{self._orchestration_bindings.rpc_base_url}' \
652
+ f'instances/{instance_id}/raiseEvent/{event_name}'
653
+
654
+ query: List[str] = []
655
+ if task_hub_name:
656
+ query.append(f'taskHub={task_hub_name}')
657
+
658
+ if connection_name:
659
+ query.append(f'connection={connection_name}')
660
+
661
+ if len(query) > 0:
662
+ request_url += "?" + "&".join(query)
663
+
664
+ return request_url
665
+
666
+ async def rewind(self,
667
+ instance_id: str,
668
+ reason: str,
669
+ task_hub_name: Optional[str] = None,
670
+ connection_name: Optional[str] = None):
671
+ """Return / "rewind" a failed orchestration instance to a prior "healthy" state.
672
+
673
+ Parameters
674
+ ----------
675
+ instance_id: str
676
+ The ID of the orchestration instance to rewind.
677
+ reason: str
678
+ The reason for rewinding the orchestration instance.
679
+ task_hub_name: Optional[str]
680
+ The TaskHub of the orchestration to rewind
681
+ connection_name: Optional[str]
682
+ Name of the application setting containing the storage
683
+ connection string to use.
684
+
685
+ Raises
686
+ ------
687
+ Exception:
688
+ In case of a failure, it reports the reason for the exception
689
+ """
690
+ request_url: str = ""
691
+ if self._orchestration_bindings.rpc_base_url:
692
+ path = f"instances/{instance_id}/rewind?reason={reason}"
693
+ query: List[str] = []
694
+ if not (task_hub_name is None):
695
+ query.append(f"taskHub={task_hub_name}")
696
+ if not (connection_name is None):
697
+ query.append(f"connection={connection_name}")
698
+ if len(query) > 0:
699
+ path += "&" + "&".join(query)
700
+
701
+ request_url = f"{self._orchestration_bindings.rpc_base_url}" + path
702
+ else:
703
+ raise Exception("The Python SDK only supports RPC endpoints."
704
+ + "Please remove the `localRpcEnabled` setting from host.json")
705
+
706
+ response = await self._post_async_request(request_url, None)
707
+ status: int = response[0]
708
+ ex_msg: str = ""
709
+ if status == 200 or status == 202:
710
+ return
711
+ elif status == 404:
712
+ ex_msg = f"No instance with ID {instance_id} found."
713
+ raise Exception(ex_msg)
714
+ elif status == 410:
715
+ ex_msg = "The rewind operation is only supported on failed orchestration instances."
716
+ raise Exception(ex_msg)
717
+ elif isinstance(response[1], str):
718
+ ex_msg = response[1]
719
+ raise Exception(ex_msg)
720
+ else:
721
+ ex_msg = "Received unexpected payload from the durable-extension: " + str(response)
722
+ raise Exception(ex_msg)
723
+
724
+ async def suspend(self, instance_id: str, reason: str) -> None:
725
+ """Suspend the specified orchestration instance.
726
+
727
+ Parameters
728
+ ----------
729
+ instance_id : str
730
+ The ID of the orchestration instance to suspend.
731
+ reason: str
732
+ The reason for suspending the instance.
733
+
734
+ Raises
735
+ ------
736
+ Exception:
737
+ When the suspend call failed with an unexpected status code
738
+
739
+ Returns
740
+ -------
741
+ None
742
+ """
743
+ request_url = f"{self._orchestration_bindings.rpc_base_url}instances/{instance_id}/" \
744
+ f"suspend?reason={quote(reason)}"
745
+ response = await self._post_async_request(request_url, None)
746
+ switch_statement = {
747
+ 202: lambda: None, # instance is suspended
748
+ 410: lambda: None, # instance completed
749
+ 404: lambda: f"No instance with ID '{instance_id}' found.",
750
+ }
751
+
752
+ has_error_message = switch_statement.get(
753
+ response[0],
754
+ lambda: f"The operation failed with an unexpected status code {response[0]}")
755
+ error_message = has_error_message()
756
+ if error_message:
757
+ raise Exception(error_message)
758
+
759
+ async def resume(self, instance_id: str, reason: str) -> None:
760
+ """Resume the specified orchestration instance.
761
+
762
+ Parameters
763
+ ----------
764
+ instance_id : str
765
+ The ID of the orchestration instance to query.
766
+ reason: str
767
+ The reason for resuming the instance.
768
+
769
+ Raises
770
+ ------
771
+ Exception:
772
+ When the resume call failed with an unexpected status code
773
+
774
+ Returns
775
+ -------
776
+ None
777
+ """
778
+ request_url = f"{self._orchestration_bindings.rpc_base_url}instances/{instance_id}/" \
779
+ f"resume?reason={quote(reason)}"
780
+ response = await self._post_async_request(request_url, None)
781
+ switch_statement = {
782
+ 202: lambda: None, # instance is resumed
783
+ 410: lambda: None, # instance completed
784
+ 404: lambda: f"No instance with ID '{instance_id}' found.",
785
+ }
786
+
787
+ has_error_message = switch_statement.get(
788
+ response[0],
789
+ lambda: f"The operation failed with an unexpected status code {response[0]}")
790
+ error_message = has_error_message()
791
+ if error_message:
792
+ raise Exception(error_message)
793
+
794
+ """Gets the current trace activity traceparent and tracestate
795
+
796
+ Returns
797
+ -------
798
+ tuple[str, str]
799
+ A tuple containing the (traceparent, tracestate)
800
+ """
801
+ @staticmethod
802
+ def _get_current_activity_context() -> tuple[str, str]:
803
+ carrier = {}
804
+
805
+ # Inject the current trace context into the carrier
806
+ TraceContextTextMapPropagator().inject(carrier)
807
+
808
+ # Extract the traceparent and optionally the tracestate
809
+ trace_parent = carrier.get("traceparent")
810
+ trace_state = carrier.get("tracestate")
811
+
812
+ return trace_parent, trace_state