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.
- azure/durable_functions/__init__.py +81 -81
- azure/durable_functions/constants.py +9 -9
- azure/durable_functions/decorators/__init__.py +3 -3
- azure/durable_functions/decorators/durable_app.py +249 -249
- azure/durable_functions/decorators/metadata.py +109 -109
- azure/durable_functions/entity.py +125 -125
- azure/durable_functions/models/DurableEntityContext.py +201 -201
- azure/durable_functions/models/DurableHttpRequest.py +58 -58
- azure/durable_functions/models/DurableOrchestrationBindings.py +66 -66
- azure/durable_functions/models/DurableOrchestrationClient.py +781 -781
- azure/durable_functions/models/DurableOrchestrationContext.py +722 -707
- azure/durable_functions/models/DurableOrchestrationStatus.py +156 -156
- azure/durable_functions/models/EntityStateResponse.py +23 -23
- azure/durable_functions/models/FunctionContext.py +7 -7
- azure/durable_functions/models/OrchestrationRuntimeStatus.py +32 -32
- azure/durable_functions/models/OrchestratorState.py +117 -116
- azure/durable_functions/models/PurgeHistoryResult.py +33 -33
- azure/durable_functions/models/ReplaySchema.py +8 -8
- azure/durable_functions/models/RetryOptions.py +69 -69
- azure/durable_functions/models/RpcManagementOptions.py +86 -86
- azure/durable_functions/models/Task.py +426 -426
- azure/durable_functions/models/TaskOrchestrationExecutor.py +346 -336
- azure/durable_functions/models/TokenSource.py +56 -56
- azure/durable_functions/models/__init__.py +24 -24
- azure/durable_functions/models/actions/Action.py +23 -23
- azure/durable_functions/models/actions/ActionType.py +18 -18
- azure/durable_functions/models/actions/CallActivityAction.py +41 -41
- azure/durable_functions/models/actions/CallActivityWithRetryAction.py +45 -45
- azure/durable_functions/models/actions/CallEntityAction.py +46 -46
- azure/durable_functions/models/actions/CallHttpAction.py +35 -35
- azure/durable_functions/models/actions/CallSubOrchestratorAction.py +40 -40
- azure/durable_functions/models/actions/CallSubOrchestratorWithRetryAction.py +44 -44
- azure/durable_functions/models/actions/CompoundAction.py +35 -35
- azure/durable_functions/models/actions/ContinueAsNewAction.py +36 -36
- azure/durable_functions/models/actions/CreateTimerAction.py +48 -48
- azure/durable_functions/models/actions/NoOpAction.py +35 -35
- azure/durable_functions/models/actions/SignalEntityAction.py +47 -47
- azure/durable_functions/models/actions/WaitForExternalEventAction.py +63 -63
- azure/durable_functions/models/actions/WhenAllAction.py +14 -14
- azure/durable_functions/models/actions/WhenAnyAction.py +14 -14
- azure/durable_functions/models/actions/__init__.py +24 -24
- azure/durable_functions/models/entities/EntityState.py +74 -74
- azure/durable_functions/models/entities/OperationResult.py +76 -76
- azure/durable_functions/models/entities/RequestMessage.py +53 -53
- azure/durable_functions/models/entities/ResponseMessage.py +48 -48
- azure/durable_functions/models/entities/Signal.py +62 -62
- azure/durable_functions/models/entities/__init__.py +17 -17
- azure/durable_functions/models/history/HistoryEvent.py +92 -92
- azure/durable_functions/models/history/HistoryEventType.py +27 -27
- azure/durable_functions/models/history/__init__.py +8 -8
- azure/durable_functions/models/utils/__init__.py +7 -7
- azure/durable_functions/models/utils/entity_utils.py +103 -91
- azure/durable_functions/models/utils/http_utils.py +69 -69
- azure/durable_functions/models/utils/json_utils.py +56 -56
- azure/durable_functions/orchestrator.py +71 -71
- {azure_functions_durable-1.2.9.dist-info → azure_functions_durable-1.2.10.dist-info}/LICENSE +21 -21
- {azure_functions_durable-1.2.9.dist-info → azure_functions_durable-1.2.10.dist-info}/METADATA +58 -58
- azure_functions_durable-1.2.10.dist-info/RECORD +100 -0
- tests/models/test_DecoratorMetadata.py +135 -135
- tests/models/test_Decorators.py +107 -107
- tests/models/test_DurableOrchestrationBindings.py +68 -68
- tests/models/test_DurableOrchestrationClient.py +730 -730
- tests/models/test_DurableOrchestrationContext.py +102 -102
- tests/models/test_DurableOrchestrationStatus.py +59 -59
- tests/models/test_OrchestrationState.py +28 -28
- tests/models/test_RpcManagementOptions.py +79 -79
- tests/models/test_TokenSource.py +10 -10
- tests/orchestrator/models/OrchestrationInstance.py +18 -18
- tests/orchestrator/orchestrator_test_utils.py +130 -130
- tests/orchestrator/schemas/OrchetrationStateSchema.py +66 -66
- tests/orchestrator/test_call_http.py +235 -176
- tests/orchestrator/test_continue_as_new.py +67 -67
- tests/orchestrator/test_create_timer.py +126 -126
- tests/orchestrator/test_entity.py +395 -395
- tests/orchestrator/test_external_event.py +53 -53
- tests/orchestrator/test_fan_out_fan_in.py +175 -175
- tests/orchestrator/test_is_replaying_flag.py +101 -101
- tests/orchestrator/test_retries.py +308 -308
- tests/orchestrator/test_sequential_orchestrator.py +841 -841
- tests/orchestrator/test_sequential_orchestrator_custom_status.py +119 -119
- tests/orchestrator/test_sequential_orchestrator_with_retry.py +465 -465
- tests/orchestrator/test_serialization.py +30 -30
- tests/orchestrator/test_sub_orchestrator.py +95 -95
- tests/orchestrator/test_sub_orchestrator_with_retry.py +129 -129
- tests/orchestrator/test_task_any.py +60 -60
- tests/tasks/tasks_test_utils.py +17 -17
- tests/tasks/test_new_uuid.py +34 -34
- tests/test_utils/ContextBuilder.py +174 -174
- tests/test_utils/EntityContextBuilder.py +56 -56
- tests/test_utils/constants.py +1 -1
- tests/test_utils/json_utils.py +30 -30
- tests/test_utils/testClasses.py +56 -56
- tests/utils/__init__.py +1 -0
- tests/utils/test_entity_utils.py +24 -0
- azure_functions_durable-1.2.9.data/data/_manifest/bsi.json +0 -1
- azure_functions_durable-1.2.9.data/data/_manifest/manifest.cat +0 -0
- azure_functions_durable-1.2.9.data/data/_manifest/manifest.spdx.json +0 -11985
- azure_functions_durable-1.2.9.data/data/_manifest/manifest.spdx.json.sha256 +0 -1
- azure_functions_durable-1.2.9.dist-info/RECORD +0 -102
- {azure_functions_durable-1.2.9.dist-info → azure_functions_durable-1.2.10.dist-info}/WHEEL +0 -0
- {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)
|