durabletask 0.1.0a1__py3-none-any.whl → 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- durabletask/__init__.py +3 -0
- durabletask/client.py +85 -24
- durabletask/entities/__init__.py +13 -0
- durabletask/entities/durable_entity.py +93 -0
- durabletask/entities/entity_context.py +154 -0
- durabletask/entities/entity_instance_id.py +40 -0
- durabletask/entities/entity_lock.py +17 -0
- durabletask/internal/entity_state_shim.py +66 -0
- durabletask/internal/exceptions.py +11 -0
- durabletask/internal/grpc_interceptor.py +65 -0
- durabletask/internal/helpers.py +85 -22
- durabletask/internal/orchestration_entity_context.py +115 -0
- durabletask/internal/orchestrator_service_pb2.py +252 -137
- durabletask/internal/orchestrator_service_pb2.pyi +1144 -0
- durabletask/internal/orchestrator_service_pb2_grpc.py +985 -396
- durabletask/internal/shared.py +46 -8
- durabletask/task.py +276 -43
- durabletask/worker.py +1762 -227
- durabletask-1.0.0.dist-info/METADATA +64 -0
- durabletask-1.0.0.dist-info/RECORD +23 -0
- {durabletask-0.1.0a1.dist-info → durabletask-1.0.0.dist-info}/WHEEL +1 -1
- durabletask/internal/__init__.py +0 -0
- durabletask-0.1.0a1.dist-info/METADATA +0 -257
- durabletask-0.1.0a1.dist-info/RECORD +0 -14
- {durabletask-0.1.0a1.dist-info → durabletask-1.0.0.dist-info/licenses}/LICENSE +0 -0
- {durabletask-0.1.0a1.dist-info → durabletask-1.0.0.dist-info}/top_level.txt +0 -0
durabletask/__init__.py
CHANGED
durabletask/client.py
CHANGED
|
@@ -4,18 +4,20 @@
|
|
|
4
4
|
import logging
|
|
5
5
|
import uuid
|
|
6
6
|
from dataclasses import dataclass
|
|
7
|
-
from datetime import datetime
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
8
|
from enum import Enum
|
|
9
|
-
from typing import Any, TypeVar, Union
|
|
9
|
+
from typing import Any, Optional, Sequence, TypeVar, Union
|
|
10
10
|
|
|
11
11
|
import grpc
|
|
12
12
|
from google.protobuf import wrappers_pb2
|
|
13
13
|
|
|
14
|
+
from durabletask.entities import EntityInstanceId
|
|
14
15
|
import durabletask.internal.helpers as helpers
|
|
15
16
|
import durabletask.internal.orchestrator_service_pb2 as pb
|
|
16
17
|
import durabletask.internal.orchestrator_service_pb2_grpc as stubs
|
|
17
18
|
import durabletask.internal.shared as shared
|
|
18
19
|
from durabletask import task
|
|
20
|
+
from durabletask.internal.grpc_interceptor import DefaultClientInterceptorImpl
|
|
19
21
|
|
|
20
22
|
TInput = TypeVar('TInput')
|
|
21
23
|
TOutput = TypeVar('TOutput')
|
|
@@ -42,10 +44,10 @@ class OrchestrationState:
|
|
|
42
44
|
runtime_status: OrchestrationStatus
|
|
43
45
|
created_at: datetime
|
|
44
46
|
last_updated_at: datetime
|
|
45
|
-
serialized_input:
|
|
46
|
-
serialized_output:
|
|
47
|
-
serialized_custom_status:
|
|
48
|
-
failure_details:
|
|
47
|
+
serialized_input: Optional[str]
|
|
48
|
+
serialized_output: Optional[str]
|
|
49
|
+
serialized_custom_status: Optional[str]
|
|
50
|
+
failure_details: Optional[task.FailureDetails]
|
|
49
51
|
|
|
50
52
|
def raise_if_failed(self):
|
|
51
53
|
if self.failure_details is not None:
|
|
@@ -64,7 +66,7 @@ class OrchestrationFailedError(Exception):
|
|
|
64
66
|
return self._failure_details
|
|
65
67
|
|
|
66
68
|
|
|
67
|
-
def new_orchestration_state(instance_id: str, res: pb.GetInstanceResponse) ->
|
|
69
|
+
def new_orchestration_state(instance_id: str, res: pb.GetInstanceResponse) -> Optional[OrchestrationState]:
|
|
68
70
|
if not res.exists:
|
|
69
71
|
return None
|
|
70
72
|
|
|
@@ -92,39 +94,66 @@ def new_orchestration_state(instance_id: str, res: pb.GetInstanceResponse) -> Un
|
|
|
92
94
|
class TaskHubGrpcClient:
|
|
93
95
|
|
|
94
96
|
def __init__(self, *,
|
|
95
|
-
host_address:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
host_address: Optional[str] = None,
|
|
98
|
+
metadata: Optional[list[tuple[str, str]]] = None,
|
|
99
|
+
log_handler: Optional[logging.Handler] = None,
|
|
100
|
+
log_formatter: Optional[logging.Formatter] = None,
|
|
101
|
+
secure_channel: bool = False,
|
|
102
|
+
interceptors: Optional[Sequence[shared.ClientInterceptor]] = None,
|
|
103
|
+
default_version: Optional[str] = None):
|
|
104
|
+
|
|
105
|
+
# If the caller provided metadata, we need to create a new interceptor for it and
|
|
106
|
+
# add it to the list of interceptors.
|
|
107
|
+
if interceptors is not None:
|
|
108
|
+
interceptors = list(interceptors)
|
|
109
|
+
if metadata is not None:
|
|
110
|
+
interceptors.append(DefaultClientInterceptorImpl(metadata))
|
|
111
|
+
elif metadata is not None:
|
|
112
|
+
interceptors = [DefaultClientInterceptorImpl(metadata)]
|
|
113
|
+
else:
|
|
114
|
+
interceptors = None
|
|
115
|
+
|
|
116
|
+
channel = shared.get_grpc_channel(
|
|
117
|
+
host_address=host_address,
|
|
118
|
+
secure_channel=secure_channel,
|
|
119
|
+
interceptors=interceptors
|
|
120
|
+
)
|
|
99
121
|
self._stub = stubs.TaskHubSidecarServiceStub(channel)
|
|
100
|
-
self._logger = shared.get_logger(log_handler, log_formatter)
|
|
122
|
+
self._logger = shared.get_logger("client", log_handler, log_formatter)
|
|
123
|
+
self.default_version = default_version
|
|
101
124
|
|
|
102
125
|
def schedule_new_orchestration(self, orchestrator: Union[task.Orchestrator[TInput, TOutput], str], *,
|
|
103
|
-
input:
|
|
104
|
-
instance_id:
|
|
105
|
-
start_at:
|
|
126
|
+
input: Optional[TInput] = None,
|
|
127
|
+
instance_id: Optional[str] = None,
|
|
128
|
+
start_at: Optional[datetime] = None,
|
|
129
|
+
reuse_id_policy: Optional[pb.OrchestrationIdReusePolicy] = None,
|
|
130
|
+
tags: Optional[dict[str, str]] = None,
|
|
131
|
+
version: Optional[str] = None) -> str:
|
|
106
132
|
|
|
107
133
|
name = orchestrator if isinstance(orchestrator, str) else task.get_name(orchestrator)
|
|
108
134
|
|
|
109
135
|
req = pb.CreateInstanceRequest(
|
|
110
136
|
name=name,
|
|
111
137
|
instanceId=instance_id if instance_id else uuid.uuid4().hex,
|
|
112
|
-
input=wrappers_pb2.StringValue(value=shared.to_json(input)) if input else None,
|
|
138
|
+
input=wrappers_pb2.StringValue(value=shared.to_json(input)) if input is not None else None,
|
|
113
139
|
scheduledStartTimestamp=helpers.new_timestamp(start_at) if start_at else None,
|
|
114
|
-
version=
|
|
140
|
+
version=helpers.get_string_value(version if version else self.default_version),
|
|
141
|
+
orchestrationIdReusePolicy=reuse_id_policy,
|
|
142
|
+
tags=tags
|
|
143
|
+
)
|
|
115
144
|
|
|
116
145
|
self._logger.info(f"Starting new '{name}' instance with ID = '{req.instanceId}'.")
|
|
117
146
|
res: pb.CreateInstanceResponse = self._stub.StartInstance(req)
|
|
118
147
|
return res.instanceId
|
|
119
148
|
|
|
120
|
-
def get_orchestration_state(self, instance_id: str, *, fetch_payloads: bool = True) ->
|
|
149
|
+
def get_orchestration_state(self, instance_id: str, *, fetch_payloads: bool = True) -> Optional[OrchestrationState]:
|
|
121
150
|
req = pb.GetInstanceRequest(instanceId=instance_id, getInputsAndOutputs=fetch_payloads)
|
|
122
151
|
res: pb.GetInstanceResponse = self._stub.GetInstance(req)
|
|
123
152
|
return new_orchestration_state(req.instanceId, res)
|
|
124
153
|
|
|
125
154
|
def wait_for_orchestration_start(self, instance_id: str, *,
|
|
126
155
|
fetch_payloads: bool = False,
|
|
127
|
-
timeout: int = 60) ->
|
|
156
|
+
timeout: int = 60) -> Optional[OrchestrationState]:
|
|
128
157
|
req = pb.GetInstanceRequest(instanceId=instance_id, getInputsAndOutputs=fetch_payloads)
|
|
129
158
|
try:
|
|
130
159
|
self._logger.info(f"Waiting up to {timeout}s for instance '{instance_id}' to start.")
|
|
@@ -139,12 +168,24 @@ class TaskHubGrpcClient:
|
|
|
139
168
|
|
|
140
169
|
def wait_for_orchestration_completion(self, instance_id: str, *,
|
|
141
170
|
fetch_payloads: bool = True,
|
|
142
|
-
timeout: int = 60) ->
|
|
171
|
+
timeout: int = 60) -> Optional[OrchestrationState]:
|
|
143
172
|
req = pb.GetInstanceRequest(instanceId=instance_id, getInputsAndOutputs=fetch_payloads)
|
|
144
173
|
try:
|
|
145
174
|
self._logger.info(f"Waiting {timeout}s for instance '{instance_id}' to complete.")
|
|
146
175
|
res: pb.GetInstanceResponse = self._stub.WaitForInstanceCompletion(req, timeout=timeout)
|
|
147
|
-
|
|
176
|
+
state = new_orchestration_state(req.instanceId, res)
|
|
177
|
+
if not state:
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
if state.runtime_status == OrchestrationStatus.FAILED and state.failure_details is not None:
|
|
181
|
+
details = state.failure_details
|
|
182
|
+
self._logger.info(f"Instance '{instance_id}' failed: [{details.error_type}] {details.message}")
|
|
183
|
+
elif state.runtime_status == OrchestrationStatus.TERMINATED:
|
|
184
|
+
self._logger.info(f"Instance '{instance_id}' was terminated.")
|
|
185
|
+
elif state.runtime_status == OrchestrationStatus.COMPLETED:
|
|
186
|
+
self._logger.info(f"Instance '{instance_id}' completed.")
|
|
187
|
+
|
|
188
|
+
return state
|
|
148
189
|
except grpc.RpcError as rpc_error:
|
|
149
190
|
if rpc_error.code() == grpc.StatusCode.DEADLINE_EXCEEDED: # type: ignore
|
|
150
191
|
# Replace gRPC error with the built-in TimeoutError
|
|
@@ -153,7 +194,7 @@ class TaskHubGrpcClient:
|
|
|
153
194
|
raise
|
|
154
195
|
|
|
155
196
|
def raise_orchestration_event(self, instance_id: str, event_name: str, *,
|
|
156
|
-
data:
|
|
197
|
+
data: Optional[Any] = None):
|
|
157
198
|
req = pb.RaiseEventRequest(
|
|
158
199
|
instanceId=instance_id,
|
|
159
200
|
name=event_name,
|
|
@@ -163,10 +204,12 @@ class TaskHubGrpcClient:
|
|
|
163
204
|
self._stub.RaiseEvent(req)
|
|
164
205
|
|
|
165
206
|
def terminate_orchestration(self, instance_id: str, *,
|
|
166
|
-
output:
|
|
207
|
+
output: Optional[Any] = None,
|
|
208
|
+
recursive: bool = True):
|
|
167
209
|
req = pb.TerminateRequest(
|
|
168
210
|
instanceId=instance_id,
|
|
169
|
-
output=wrappers_pb2.StringValue(value=shared.to_json(output)) if output else None
|
|
211
|
+
output=wrappers_pb2.StringValue(value=shared.to_json(output)) if output else None,
|
|
212
|
+
recursive=recursive)
|
|
170
213
|
|
|
171
214
|
self._logger.info(f"Terminating instance '{instance_id}'.")
|
|
172
215
|
self._stub.TerminateInstance(req)
|
|
@@ -180,3 +223,21 @@ class TaskHubGrpcClient:
|
|
|
180
223
|
req = pb.ResumeRequest(instanceId=instance_id)
|
|
181
224
|
self._logger.info(f"Resuming instance '{instance_id}'.")
|
|
182
225
|
self._stub.ResumeInstance(req)
|
|
226
|
+
|
|
227
|
+
def purge_orchestration(self, instance_id: str, recursive: bool = True):
|
|
228
|
+
req = pb.PurgeInstancesRequest(instanceId=instance_id, recursive=recursive)
|
|
229
|
+
self._logger.info(f"Purging instance '{instance_id}'.")
|
|
230
|
+
self._stub.PurgeInstances(req)
|
|
231
|
+
|
|
232
|
+
def signal_entity(self, entity_instance_id: EntityInstanceId, operation_name: str, input: Optional[Any] = None):
|
|
233
|
+
req = pb.SignalEntityRequest(
|
|
234
|
+
instanceId=str(entity_instance_id),
|
|
235
|
+
name=operation_name,
|
|
236
|
+
input=wrappers_pb2.StringValue(value=shared.to_json(input)) if input else None,
|
|
237
|
+
requestId=str(uuid.uuid4()),
|
|
238
|
+
scheduledTime=None,
|
|
239
|
+
parentTraceContext=None,
|
|
240
|
+
requestTime=helpers.new_timestamp(datetime.now(timezone.utc))
|
|
241
|
+
)
|
|
242
|
+
self._logger.info(f"Signaling entity '{entity_instance_id}' operation '{operation_name}'.")
|
|
243
|
+
self._stub.SignalEntity(req, None) # TODO: Cancellation timeout?
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
"""Durable Task SDK for Python entities component"""
|
|
5
|
+
|
|
6
|
+
from durabletask.entities.entity_instance_id import EntityInstanceId
|
|
7
|
+
from durabletask.entities.durable_entity import DurableEntity
|
|
8
|
+
from durabletask.entities.entity_lock import EntityLock
|
|
9
|
+
from durabletask.entities.entity_context import EntityContext
|
|
10
|
+
|
|
11
|
+
__all__ = ["EntityInstanceId", "DurableEntity", "EntityLock", "EntityContext"]
|
|
12
|
+
|
|
13
|
+
PACKAGE_NAME = "durabletask.entities"
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from typing import Any, Optional, Type, TypeVar, Union, overload
|
|
2
|
+
|
|
3
|
+
from durabletask.entities.entity_context import EntityContext
|
|
4
|
+
from durabletask.entities.entity_instance_id import EntityInstanceId
|
|
5
|
+
|
|
6
|
+
TState = TypeVar("TState")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DurableEntity:
|
|
10
|
+
def _initialize_entity_context(self, context: EntityContext):
|
|
11
|
+
self.entity_context = context
|
|
12
|
+
|
|
13
|
+
@overload
|
|
14
|
+
def get_state(self, intended_type: Type[TState], default: TState) -> TState:
|
|
15
|
+
...
|
|
16
|
+
|
|
17
|
+
@overload
|
|
18
|
+
def get_state(self, intended_type: Type[TState]) -> Optional[TState]:
|
|
19
|
+
...
|
|
20
|
+
|
|
21
|
+
@overload
|
|
22
|
+
def get_state(self, intended_type: None = None, default: Any = None) -> Any:
|
|
23
|
+
...
|
|
24
|
+
|
|
25
|
+
def get_state(self, intended_type: Optional[Type[TState]] = None, default: Optional[TState] = None) -> Union[None, TState, Any]:
|
|
26
|
+
"""Get the current state of the entity, optionally converting it to a specified type.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
intended_type : Type[TState] | None, optional
|
|
31
|
+
The type to which the state should be converted. If None, the state is returned as-is.
|
|
32
|
+
default : TState, optional
|
|
33
|
+
The default value to return if the state is not found or cannot be converted.
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
TState | Any
|
|
38
|
+
The current state of the entity, optionally converted to the specified type.
|
|
39
|
+
"""
|
|
40
|
+
return self.entity_context.get_state(intended_type, default)
|
|
41
|
+
|
|
42
|
+
def set_state(self, state: Any):
|
|
43
|
+
"""Set the state of the entity to a new value.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
new_state : Any
|
|
48
|
+
The new state to set for the entity.
|
|
49
|
+
"""
|
|
50
|
+
self.entity_context.set_state(state)
|
|
51
|
+
|
|
52
|
+
def signal_entity(self, entity_instance_id: EntityInstanceId, operation: str, input: Optional[Any] = None) -> None:
|
|
53
|
+
"""Signal another entity to perform an operation.
|
|
54
|
+
|
|
55
|
+
Parameters
|
|
56
|
+
----------
|
|
57
|
+
entity_instance_id : EntityInstanceId
|
|
58
|
+
The ID of the entity instance to signal.
|
|
59
|
+
operation : str
|
|
60
|
+
The operation to perform on the entity.
|
|
61
|
+
input : Any, optional
|
|
62
|
+
The input to provide to the entity for the operation.
|
|
63
|
+
"""
|
|
64
|
+
self.entity_context.signal_entity(entity_instance_id, operation, input)
|
|
65
|
+
|
|
66
|
+
def schedule_new_orchestration(self, orchestration_name: str, input: Optional[Any] = None, instance_id: Optional[str] = None) -> str:
|
|
67
|
+
"""Schedule a new orchestration instance.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
orchestration_name : str
|
|
72
|
+
The name of the orchestration to schedule.
|
|
73
|
+
input : Any, optional
|
|
74
|
+
The input to provide to the new orchestration.
|
|
75
|
+
instance_id : str, optional
|
|
76
|
+
The instance ID to assign to the new orchestration. If None, a new ID will be generated.
|
|
77
|
+
|
|
78
|
+
Returns
|
|
79
|
+
-------
|
|
80
|
+
str
|
|
81
|
+
The instance ID of the scheduled orchestration.
|
|
82
|
+
"""
|
|
83
|
+
return self.entity_context.schedule_new_orchestration(orchestration_name, input, instance_id=instance_id)
|
|
84
|
+
|
|
85
|
+
def delete(self, input: Any = None) -> None:
|
|
86
|
+
"""Delete the entity instance.
|
|
87
|
+
|
|
88
|
+
Parameters
|
|
89
|
+
----------
|
|
90
|
+
input : Any, optional
|
|
91
|
+
Unused: The input for the entity "delete" operation.
|
|
92
|
+
"""
|
|
93
|
+
self.set_state(None)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
|
|
2
|
+
from typing import Any, Optional, Type, TypeVar, Union, overload
|
|
3
|
+
import uuid
|
|
4
|
+
from durabletask.entities.entity_instance_id import EntityInstanceId
|
|
5
|
+
from durabletask.internal import helpers, shared
|
|
6
|
+
from durabletask.internal.entity_state_shim import StateShim
|
|
7
|
+
import durabletask.internal.orchestrator_service_pb2 as pb
|
|
8
|
+
|
|
9
|
+
TState = TypeVar("TState")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EntityContext:
|
|
13
|
+
def __init__(self, orchestration_id: str, operation: str, state: StateShim, entity_id: EntityInstanceId):
|
|
14
|
+
self._orchestration_id = orchestration_id
|
|
15
|
+
self._operation = operation
|
|
16
|
+
self._state = state
|
|
17
|
+
self._entity_id = entity_id
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def orchestration_id(self) -> str:
|
|
21
|
+
"""Get the ID of the orchestration instance that scheduled this entity.
|
|
22
|
+
|
|
23
|
+
Returns
|
|
24
|
+
-------
|
|
25
|
+
str
|
|
26
|
+
The ID of the current orchestration instance.
|
|
27
|
+
"""
|
|
28
|
+
return self._orchestration_id
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def operation(self) -> str:
|
|
32
|
+
"""Get the operation associated with this entity invocation.
|
|
33
|
+
|
|
34
|
+
The operation is a string that identifies the specific action being
|
|
35
|
+
performed on the entity. It can be used to distinguish between
|
|
36
|
+
multiple operations that are part of the same entity invocation.
|
|
37
|
+
|
|
38
|
+
Returns
|
|
39
|
+
-------
|
|
40
|
+
str
|
|
41
|
+
The operation associated with this entity invocation.
|
|
42
|
+
"""
|
|
43
|
+
return self._operation
|
|
44
|
+
|
|
45
|
+
@overload
|
|
46
|
+
def get_state(self, intended_type: Type[TState], default: TState) -> TState:
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
@overload
|
|
50
|
+
def get_state(self, intended_type: Type[TState]) -> Optional[TState]:
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
@overload
|
|
54
|
+
def get_state(self, intended_type: None = None, default: Any = None) -> Any:
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
def get_state(self, intended_type: Optional[Type[TState]] = None, default: Optional[TState] = None) -> Union[None, TState, Any]:
|
|
58
|
+
"""Get the current state of the entity, optionally converting it to a specified type.
|
|
59
|
+
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
intended_type : Type[TState] | None, optional
|
|
63
|
+
The type to which the state should be converted. If None, the state is returned as-is.
|
|
64
|
+
default : TState, optional
|
|
65
|
+
The default value to return if the state is not found or cannot be converted.
|
|
66
|
+
|
|
67
|
+
Returns
|
|
68
|
+
-------
|
|
69
|
+
TState | Any
|
|
70
|
+
The current state of the entity, optionally converted to the specified type.
|
|
71
|
+
"""
|
|
72
|
+
return self._state.get_state(intended_type, default)
|
|
73
|
+
|
|
74
|
+
def set_state(self, new_state: Any):
|
|
75
|
+
"""Set the state of the entity to a new value.
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
new_state : Any
|
|
80
|
+
The new state to set for the entity.
|
|
81
|
+
"""
|
|
82
|
+
self._state.set_state(new_state)
|
|
83
|
+
|
|
84
|
+
def signal_entity(self, entity_instance_id: EntityInstanceId, operation: str, input: Optional[Any] = None) -> None:
|
|
85
|
+
"""Signal another entity to perform an operation.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
entity_instance_id : EntityInstanceId
|
|
90
|
+
The ID of the entity instance to signal.
|
|
91
|
+
operation : str
|
|
92
|
+
The operation to perform on the entity.
|
|
93
|
+
input : Any, optional
|
|
94
|
+
The input to provide to the entity for the operation.
|
|
95
|
+
"""
|
|
96
|
+
encoded_input = shared.to_json(input) if input is not None else None
|
|
97
|
+
self._state.add_operation_action(
|
|
98
|
+
pb.OperationAction(
|
|
99
|
+
sendSignal=pb.SendSignalAction(
|
|
100
|
+
instanceId=str(entity_instance_id),
|
|
101
|
+
name=operation,
|
|
102
|
+
input=helpers.get_string_value(encoded_input),
|
|
103
|
+
scheduledTime=None,
|
|
104
|
+
requestTime=None,
|
|
105
|
+
parentTraceContext=None,
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def schedule_new_orchestration(self, orchestration_name: str, input: Optional[Any] = None, instance_id: Optional[str] = None) -> str:
|
|
111
|
+
"""Schedule a new orchestration instance.
|
|
112
|
+
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
orchestration_name : str
|
|
116
|
+
The name of the orchestration to schedule.
|
|
117
|
+
input : Any, optional
|
|
118
|
+
The input to provide to the new orchestration.
|
|
119
|
+
instance_id : str, optional
|
|
120
|
+
The instance ID to assign to the new orchestration. If None, a new ID will be generated.
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
str
|
|
125
|
+
The instance ID of the scheduled orchestration.
|
|
126
|
+
"""
|
|
127
|
+
encoded_input = shared.to_json(input) if input is not None else None
|
|
128
|
+
if not instance_id:
|
|
129
|
+
instance_id = uuid.uuid4().hex
|
|
130
|
+
self._state.add_operation_action(
|
|
131
|
+
pb.OperationAction(
|
|
132
|
+
startNewOrchestration=pb.StartNewOrchestrationAction(
|
|
133
|
+
instanceId=instance_id,
|
|
134
|
+
name=orchestration_name,
|
|
135
|
+
input=helpers.get_string_value(encoded_input),
|
|
136
|
+
version=None,
|
|
137
|
+
scheduledTime=None,
|
|
138
|
+
requestTime=None,
|
|
139
|
+
parentTraceContext=None
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
return instance_id
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def entity_id(self) -> EntityInstanceId:
|
|
147
|
+
"""Get the ID of the entity instance.
|
|
148
|
+
|
|
149
|
+
Returns
|
|
150
|
+
-------
|
|
151
|
+
str
|
|
152
|
+
The ID of the current entity instance.
|
|
153
|
+
"""
|
|
154
|
+
return self._entity_id
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class EntityInstanceId:
|
|
5
|
+
def __init__(self, entity: str, key: str):
|
|
6
|
+
self.entity = entity
|
|
7
|
+
self.key = key
|
|
8
|
+
|
|
9
|
+
def __str__(self) -> str:
|
|
10
|
+
return f"@{self.entity}@{self.key}"
|
|
11
|
+
|
|
12
|
+
def __eq__(self, other):
|
|
13
|
+
if not isinstance(other, EntityInstanceId):
|
|
14
|
+
return False
|
|
15
|
+
return self.entity == other.entity and self.key == other.key
|
|
16
|
+
|
|
17
|
+
def __lt__(self, other):
|
|
18
|
+
if not isinstance(other, EntityInstanceId):
|
|
19
|
+
return self < other
|
|
20
|
+
return str(self) < str(other)
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def parse(entity_id: str) -> Optional["EntityInstanceId"]:
|
|
24
|
+
"""Parse a string representation of an entity ID into an EntityInstanceId object.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
entity_id : str
|
|
29
|
+
The string representation of the entity ID, in the format '@entity@key'.
|
|
30
|
+
|
|
31
|
+
Returns
|
|
32
|
+
-------
|
|
33
|
+
Optional[EntityInstanceId]
|
|
34
|
+
The parsed EntityInstanceId object, or None if the input is None.
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
_, entity, key = entity_id.split("@", 2)
|
|
38
|
+
return EntityInstanceId(entity=entity, key=key)
|
|
39
|
+
except ValueError as ex:
|
|
40
|
+
raise ValueError("Invalid entity ID format", ex)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from durabletask.task import OrchestrationContext
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EntityLock:
|
|
9
|
+
# Note: This should
|
|
10
|
+
def __init__(self, context: 'OrchestrationContext'):
|
|
11
|
+
self._context = context
|
|
12
|
+
|
|
13
|
+
def __enter__(self):
|
|
14
|
+
return self
|
|
15
|
+
|
|
16
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
17
|
+
self._context._exit_critical_section()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from typing import Any, TypeVar, Union
|
|
2
|
+
from typing import Optional, Type, overload
|
|
3
|
+
|
|
4
|
+
import durabletask.internal.orchestrator_service_pb2 as pb
|
|
5
|
+
|
|
6
|
+
TState = TypeVar("TState")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class StateShim:
|
|
10
|
+
def __init__(self, start_state):
|
|
11
|
+
self._current_state: Any = start_state
|
|
12
|
+
self._checkpoint_state: Any = start_state
|
|
13
|
+
self._operation_actions: list[pb.OperationAction] = []
|
|
14
|
+
self._actions_checkpoint_state: int = 0
|
|
15
|
+
|
|
16
|
+
@overload
|
|
17
|
+
def get_state(self, intended_type: Type[TState], default: TState) -> TState:
|
|
18
|
+
...
|
|
19
|
+
|
|
20
|
+
@overload
|
|
21
|
+
def get_state(self, intended_type: Type[TState]) -> Optional[TState]:
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
@overload
|
|
25
|
+
def get_state(self, intended_type: None = None, default: Any = None) -> Any:
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
def get_state(self, intended_type: Optional[Type[TState]] = None, default: Optional[TState] = None) -> Union[None, TState, Any]:
|
|
29
|
+
if self._current_state is None and default is not None:
|
|
30
|
+
return default
|
|
31
|
+
|
|
32
|
+
if intended_type is None:
|
|
33
|
+
return self._current_state
|
|
34
|
+
|
|
35
|
+
if isinstance(self._current_state, intended_type):
|
|
36
|
+
return self._current_state
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
return intended_type(self._current_state) # type: ignore[call-arg]
|
|
40
|
+
except Exception as ex:
|
|
41
|
+
raise TypeError(
|
|
42
|
+
f"Could not convert state of type '{type(self._current_state).__name__}' to '{intended_type.__name__}'"
|
|
43
|
+
) from ex
|
|
44
|
+
|
|
45
|
+
def set_state(self, state):
|
|
46
|
+
self._current_state = state
|
|
47
|
+
|
|
48
|
+
def add_operation_action(self, action: pb.OperationAction):
|
|
49
|
+
self._operation_actions.append(action)
|
|
50
|
+
|
|
51
|
+
def get_operation_actions(self) -> list[pb.OperationAction]:
|
|
52
|
+
return self._operation_actions[:self._actions_checkpoint_state]
|
|
53
|
+
|
|
54
|
+
def commit(self):
|
|
55
|
+
self._checkpoint_state = self._current_state
|
|
56
|
+
self._actions_checkpoint_state = len(self._operation_actions)
|
|
57
|
+
|
|
58
|
+
def rollback(self):
|
|
59
|
+
self._current_state = self._checkpoint_state
|
|
60
|
+
self._operation_actions = self._operation_actions[:self._actions_checkpoint_state]
|
|
61
|
+
|
|
62
|
+
def reset(self):
|
|
63
|
+
self._current_state = None
|
|
64
|
+
self._checkpoint_state = None
|
|
65
|
+
self._operation_actions = []
|
|
66
|
+
self._actions_checkpoint_state = 0
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import durabletask.internal.orchestrator_service_pb2 as pb
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class VersionFailureException(Exception):
|
|
5
|
+
def __init__(self, error_details: pb.TaskFailureDetails) -> None:
|
|
6
|
+
super().__init__()
|
|
7
|
+
self.error_details = error_details
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AbandonOrchestrationError(Exception):
|
|
11
|
+
pass
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
from collections import namedtuple
|
|
5
|
+
|
|
6
|
+
import grpc
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class _ClientCallDetails(
|
|
10
|
+
namedtuple(
|
|
11
|
+
'_ClientCallDetails',
|
|
12
|
+
['method', 'timeout', 'metadata', 'credentials', 'wait_for_ready', 'compression']),
|
|
13
|
+
grpc.ClientCallDetails):
|
|
14
|
+
"""This is an implementation of the ClientCallDetails interface needed for interceptors.
|
|
15
|
+
This class takes six named values and inherits the ClientCallDetails from grpc package.
|
|
16
|
+
This class encloses the values that describe a RPC to be invoked.
|
|
17
|
+
"""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DefaultClientInterceptorImpl (
|
|
22
|
+
grpc.UnaryUnaryClientInterceptor, grpc.UnaryStreamClientInterceptor,
|
|
23
|
+
grpc.StreamUnaryClientInterceptor, grpc.StreamStreamClientInterceptor):
|
|
24
|
+
"""The class implements a UnaryUnaryClientInterceptor, UnaryStreamClientInterceptor,
|
|
25
|
+
StreamUnaryClientInterceptor and StreamStreamClientInterceptor from grpc to add an
|
|
26
|
+
interceptor to add additional headers to all calls as needed."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, metadata: list[tuple[str, str]]):
|
|
29
|
+
super().__init__()
|
|
30
|
+
self._metadata = metadata
|
|
31
|
+
|
|
32
|
+
def _intercept_call(
|
|
33
|
+
self, client_call_details: _ClientCallDetails) -> grpc.ClientCallDetails:
|
|
34
|
+
"""Internal intercept_call implementation which adds metadata to grpc metadata in the RPC
|
|
35
|
+
call details."""
|
|
36
|
+
if self._metadata is None:
|
|
37
|
+
return client_call_details
|
|
38
|
+
|
|
39
|
+
if client_call_details.metadata is not None:
|
|
40
|
+
metadata = list(client_call_details.metadata)
|
|
41
|
+
else:
|
|
42
|
+
metadata = []
|
|
43
|
+
|
|
44
|
+
metadata.extend(self._metadata)
|
|
45
|
+
client_call_details = _ClientCallDetails(
|
|
46
|
+
client_call_details.method, client_call_details.timeout, metadata,
|
|
47
|
+
client_call_details.credentials, client_call_details.wait_for_ready, client_call_details.compression)
|
|
48
|
+
|
|
49
|
+
return client_call_details
|
|
50
|
+
|
|
51
|
+
def intercept_unary_unary(self, continuation, client_call_details, request):
|
|
52
|
+
new_client_call_details = self._intercept_call(client_call_details)
|
|
53
|
+
return continuation(new_client_call_details, request)
|
|
54
|
+
|
|
55
|
+
def intercept_unary_stream(self, continuation, client_call_details, request):
|
|
56
|
+
new_client_call_details = self._intercept_call(client_call_details)
|
|
57
|
+
return continuation(new_client_call_details, request)
|
|
58
|
+
|
|
59
|
+
def intercept_stream_unary(self, continuation, client_call_details, request):
|
|
60
|
+
new_client_call_details = self._intercept_call(client_call_details)
|
|
61
|
+
return continuation(new_client_call_details, request)
|
|
62
|
+
|
|
63
|
+
def intercept_stream_stream(self, continuation, client_call_details, request):
|
|
64
|
+
new_client_call_details = self._intercept_call(client_call_details)
|
|
65
|
+
return continuation(new_client_call_details, request)
|