azure-functions-durable 1.2.9__py3-none-any.whl → 1.2.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. azure/durable_functions/__init__.py +81 -81
  2. azure/durable_functions/constants.py +9 -9
  3. azure/durable_functions/decorators/__init__.py +3 -3
  4. azure/durable_functions/decorators/durable_app.py +249 -249
  5. azure/durable_functions/decorators/metadata.py +109 -109
  6. azure/durable_functions/entity.py +125 -125
  7. azure/durable_functions/models/DurableEntityContext.py +201 -201
  8. azure/durable_functions/models/DurableHttpRequest.py +58 -58
  9. azure/durable_functions/models/DurableOrchestrationBindings.py +66 -66
  10. azure/durable_functions/models/DurableOrchestrationClient.py +781 -781
  11. azure/durable_functions/models/DurableOrchestrationContext.py +722 -707
  12. azure/durable_functions/models/DurableOrchestrationStatus.py +156 -156
  13. azure/durable_functions/models/EntityStateResponse.py +23 -23
  14. azure/durable_functions/models/FunctionContext.py +7 -7
  15. azure/durable_functions/models/OrchestrationRuntimeStatus.py +32 -32
  16. azure/durable_functions/models/OrchestratorState.py +117 -116
  17. azure/durable_functions/models/PurgeHistoryResult.py +33 -33
  18. azure/durable_functions/models/ReplaySchema.py +8 -8
  19. azure/durable_functions/models/RetryOptions.py +69 -69
  20. azure/durable_functions/models/RpcManagementOptions.py +86 -86
  21. azure/durable_functions/models/Task.py +426 -426
  22. azure/durable_functions/models/TaskOrchestrationExecutor.py +346 -336
  23. azure/durable_functions/models/TokenSource.py +56 -56
  24. azure/durable_functions/models/__init__.py +24 -24
  25. azure/durable_functions/models/actions/Action.py +23 -23
  26. azure/durable_functions/models/actions/ActionType.py +18 -18
  27. azure/durable_functions/models/actions/CallActivityAction.py +41 -41
  28. azure/durable_functions/models/actions/CallActivityWithRetryAction.py +45 -45
  29. azure/durable_functions/models/actions/CallEntityAction.py +46 -46
  30. azure/durable_functions/models/actions/CallHttpAction.py +35 -35
  31. azure/durable_functions/models/actions/CallSubOrchestratorAction.py +40 -40
  32. azure/durable_functions/models/actions/CallSubOrchestratorWithRetryAction.py +44 -44
  33. azure/durable_functions/models/actions/CompoundAction.py +35 -35
  34. azure/durable_functions/models/actions/ContinueAsNewAction.py +36 -36
  35. azure/durable_functions/models/actions/CreateTimerAction.py +48 -48
  36. azure/durable_functions/models/actions/NoOpAction.py +35 -35
  37. azure/durable_functions/models/actions/SignalEntityAction.py +47 -47
  38. azure/durable_functions/models/actions/WaitForExternalEventAction.py +63 -63
  39. azure/durable_functions/models/actions/WhenAllAction.py +14 -14
  40. azure/durable_functions/models/actions/WhenAnyAction.py +14 -14
  41. azure/durable_functions/models/actions/__init__.py +24 -24
  42. azure/durable_functions/models/entities/EntityState.py +74 -74
  43. azure/durable_functions/models/entities/OperationResult.py +76 -76
  44. azure/durable_functions/models/entities/RequestMessage.py +53 -53
  45. azure/durable_functions/models/entities/ResponseMessage.py +48 -48
  46. azure/durable_functions/models/entities/Signal.py +62 -62
  47. azure/durable_functions/models/entities/__init__.py +17 -17
  48. azure/durable_functions/models/history/HistoryEvent.py +92 -92
  49. azure/durable_functions/models/history/HistoryEventType.py +27 -27
  50. azure/durable_functions/models/history/__init__.py +8 -8
  51. azure/durable_functions/models/utils/__init__.py +7 -7
  52. azure/durable_functions/models/utils/entity_utils.py +103 -91
  53. azure/durable_functions/models/utils/http_utils.py +69 -69
  54. azure/durable_functions/models/utils/json_utils.py +56 -56
  55. azure/durable_functions/orchestrator.py +71 -71
  56. {azure_functions_durable-1.2.9.dist-info → azure_functions_durable-1.2.10.dist-info}/LICENSE +21 -21
  57. {azure_functions_durable-1.2.9.dist-info → azure_functions_durable-1.2.10.dist-info}/METADATA +58 -58
  58. azure_functions_durable-1.2.10.dist-info/RECORD +100 -0
  59. tests/models/test_DecoratorMetadata.py +135 -135
  60. tests/models/test_Decorators.py +107 -107
  61. tests/models/test_DurableOrchestrationBindings.py +68 -68
  62. tests/models/test_DurableOrchestrationClient.py +730 -730
  63. tests/models/test_DurableOrchestrationContext.py +102 -102
  64. tests/models/test_DurableOrchestrationStatus.py +59 -59
  65. tests/models/test_OrchestrationState.py +28 -28
  66. tests/models/test_RpcManagementOptions.py +79 -79
  67. tests/models/test_TokenSource.py +10 -10
  68. tests/orchestrator/models/OrchestrationInstance.py +18 -18
  69. tests/orchestrator/orchestrator_test_utils.py +130 -130
  70. tests/orchestrator/schemas/OrchetrationStateSchema.py +66 -66
  71. tests/orchestrator/test_call_http.py +235 -176
  72. tests/orchestrator/test_continue_as_new.py +67 -67
  73. tests/orchestrator/test_create_timer.py +126 -126
  74. tests/orchestrator/test_entity.py +395 -395
  75. tests/orchestrator/test_external_event.py +53 -53
  76. tests/orchestrator/test_fan_out_fan_in.py +175 -175
  77. tests/orchestrator/test_is_replaying_flag.py +101 -101
  78. tests/orchestrator/test_retries.py +308 -308
  79. tests/orchestrator/test_sequential_orchestrator.py +841 -841
  80. tests/orchestrator/test_sequential_orchestrator_custom_status.py +119 -119
  81. tests/orchestrator/test_sequential_orchestrator_with_retry.py +465 -465
  82. tests/orchestrator/test_serialization.py +30 -30
  83. tests/orchestrator/test_sub_orchestrator.py +95 -95
  84. tests/orchestrator/test_sub_orchestrator_with_retry.py +129 -129
  85. tests/orchestrator/test_task_any.py +60 -60
  86. tests/tasks/tasks_test_utils.py +17 -17
  87. tests/tasks/test_new_uuid.py +34 -34
  88. tests/test_utils/ContextBuilder.py +174 -174
  89. tests/test_utils/EntityContextBuilder.py +56 -56
  90. tests/test_utils/constants.py +1 -1
  91. tests/test_utils/json_utils.py +30 -30
  92. tests/test_utils/testClasses.py +56 -56
  93. tests/utils/__init__.py +1 -0
  94. tests/utils/test_entity_utils.py +24 -0
  95. azure_functions_durable-1.2.9.data/data/_manifest/bsi.json +0 -1
  96. azure_functions_durable-1.2.9.data/data/_manifest/manifest.cat +0 -0
  97. azure_functions_durable-1.2.9.data/data/_manifest/manifest.spdx.json +0 -11985
  98. azure_functions_durable-1.2.9.data/data/_manifest/manifest.spdx.json.sha256 +0 -1
  99. azure_functions_durable-1.2.9.dist-info/RECORD +0 -102
  100. {azure_functions_durable-1.2.9.dist-info → azure_functions_durable-1.2.10.dist-info}/WHEEL +0 -0
  101. {azure_functions_durable-1.2.9.dist-info → azure_functions_durable-1.2.10.dist-info}/top_level.txt +0 -0
@@ -1,781 +1,781 @@
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
+
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)