durabletask 0.0.0.dev1__tar.gz
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-0.0.0.dev1/LICENSE +21 -0
- durabletask-0.0.0.dev1/PKG-INFO +64 -0
- durabletask-0.0.0.dev1/README.md +23 -0
- durabletask-0.0.0.dev1/durabletask/__init__.py +10 -0
- durabletask-0.0.0.dev1/durabletask/client.py +256 -0
- durabletask-0.0.0.dev1/durabletask/entities/__init__.py +14 -0
- durabletask-0.0.0.dev1/durabletask/entities/durable_entity.py +93 -0
- durabletask-0.0.0.dev1/durabletask/entities/entity_context.py +154 -0
- durabletask-0.0.0.dev1/durabletask/entities/entity_instance_id.py +40 -0
- durabletask-0.0.0.dev1/durabletask/entities/entity_lock.py +17 -0
- durabletask-0.0.0.dev1/durabletask/entities/entity_metadata.py +97 -0
- durabletask-0.0.0.dev1/durabletask/internal/ProtoTaskHubSidecarServiceStub.py +34 -0
- durabletask-0.0.0.dev1/durabletask/internal/entity_state_shim.py +66 -0
- durabletask-0.0.0.dev1/durabletask/internal/exceptions.py +11 -0
- durabletask-0.0.0.dev1/durabletask/internal/grpc_interceptor.py +65 -0
- durabletask-0.0.0.dev1/durabletask/internal/helpers.py +288 -0
- durabletask-0.0.0.dev1/durabletask/internal/orchestration_entity_context.py +115 -0
- durabletask-0.0.0.dev1/durabletask/internal/orchestrator_service_pb2.py +270 -0
- durabletask-0.0.0.dev1/durabletask/internal/orchestrator_service_pb2.pyi +1144 -0
- durabletask-0.0.0.dev1/durabletask/internal/orchestrator_service_pb2_grpc.py +1200 -0
- durabletask-0.0.0.dev1/durabletask/internal/shared.py +129 -0
- durabletask-0.0.0.dev1/durabletask/task.py +621 -0
- durabletask-0.0.0.dev1/durabletask/worker.py +2327 -0
- durabletask-0.0.0.dev1/durabletask.egg-info/PKG-INFO +64 -0
- durabletask-0.0.0.dev1/durabletask.egg-info/SOURCES.txt +28 -0
- durabletask-0.0.0.dev1/durabletask.egg-info/dependency_links.txt +1 -0
- durabletask-0.0.0.dev1/durabletask.egg-info/requires.txt +4 -0
- durabletask-0.0.0.dev1/durabletask.egg-info/top_level.txt +1 -0
- durabletask-0.0.0.dev1/pyproject.toml +46 -0
- durabletask-0.0.0.dev1/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Microsoft Corporation.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: durabletask
|
|
3
|
+
Version: 0.0.0.dev1
|
|
4
|
+
Summary: A Durable Task Client SDK for Python
|
|
5
|
+
License: MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) Microsoft Corporation.
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE
|
|
26
|
+
|
|
27
|
+
Project-URL: repository, https://github.com/microsoft/durabletask-python
|
|
28
|
+
Project-URL: changelog, https://github.com/microsoft/durabletask-python/blob/main/CHANGELOG.md
|
|
29
|
+
Keywords: durable,task,workflow
|
|
30
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
31
|
+
Classifier: Programming Language :: Python :: 3
|
|
32
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
33
|
+
Requires-Python: >=3.10
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
License-File: LICENSE
|
|
36
|
+
Requires-Dist: grpcio
|
|
37
|
+
Requires-Dist: protobuf
|
|
38
|
+
Requires-Dist: asyncio
|
|
39
|
+
Requires-Dist: packaging
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
|
|
42
|
+
# Durable Task SDK for Python
|
|
43
|
+
|
|
44
|
+
[](https://opensource.org/licenses/MIT)
|
|
45
|
+
[](https://github.com/microsoft/durabletask-python/actions/workflows/pr-validation.yml)
|
|
46
|
+
[](https://badge.fury.io/py/durabletask)
|
|
47
|
+
|
|
48
|
+
This repo contains a Python SDK for use with the [Azure Durable Task Scheduler](https://github.com/Azure/Durable-Task-Scheduler). With this SDK, you can define, schedule, and manage durable orchestrations using ordinary Python code.
|
|
49
|
+
|
|
50
|
+
> Note that this SDK is **not** currently compatible with [Azure Durable Functions](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-overview). If you are looking for a Python SDK for Azure Durable Functions, please see [this repo](https://github.com/Azure/azure-functions-durable-python).
|
|
51
|
+
|
|
52
|
+
# References
|
|
53
|
+
- [Supported Patterns](./docs/supported-patterns.md)
|
|
54
|
+
- [Available Features](./docs/features.md)
|
|
55
|
+
- [Getting Started](./docs/getting-started.md)
|
|
56
|
+
- [Development Guide](./docs/development.md)
|
|
57
|
+
- [Contributing Guide](./CONTRIBUTING.md)
|
|
58
|
+
|
|
59
|
+
## Trademarks
|
|
60
|
+
This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft
|
|
61
|
+
trademarks or logos is subject to and must follow
|
|
62
|
+
[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general).
|
|
63
|
+
Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.
|
|
64
|
+
Any use of third-party trademarks or logos are subject to those third-party's policies.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Durable Task SDK for Python
|
|
2
|
+
|
|
3
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
|
+
[](https://github.com/microsoft/durabletask-python/actions/workflows/pr-validation.yml)
|
|
5
|
+
[](https://badge.fury.io/py/durabletask)
|
|
6
|
+
|
|
7
|
+
This repo contains a Python SDK for use with the [Azure Durable Task Scheduler](https://github.com/Azure/Durable-Task-Scheduler). With this SDK, you can define, schedule, and manage durable orchestrations using ordinary Python code.
|
|
8
|
+
|
|
9
|
+
> Note that this SDK is **not** currently compatible with [Azure Durable Functions](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-overview). If you are looking for a Python SDK for Azure Durable Functions, please see [this repo](https://github.com/Azure/azure-functions-durable-python).
|
|
10
|
+
|
|
11
|
+
# References
|
|
12
|
+
- [Supported Patterns](./docs/supported-patterns.md)
|
|
13
|
+
- [Available Features](./docs/features.md)
|
|
14
|
+
- [Getting Started](./docs/getting-started.md)
|
|
15
|
+
- [Development Guide](./docs/development.md)
|
|
16
|
+
- [Contributing Guide](./CONTRIBUTING.md)
|
|
17
|
+
|
|
18
|
+
## Trademarks
|
|
19
|
+
This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft
|
|
20
|
+
trademarks or logos is subject to and must follow
|
|
21
|
+
[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general).
|
|
22
|
+
Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.
|
|
23
|
+
Any use of third-party trademarks or logos are subject to those third-party's policies.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
"""Durable Task SDK for Python"""
|
|
5
|
+
|
|
6
|
+
from durabletask.worker import ConcurrencyOptions, VersioningOptions
|
|
7
|
+
|
|
8
|
+
__all__ = ["ConcurrencyOptions", "VersioningOptions"]
|
|
9
|
+
|
|
10
|
+
PACKAGE_NAME = "durabletask"
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import uuid
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Any, Optional, Sequence, TypeVar, Union
|
|
10
|
+
|
|
11
|
+
import grpc
|
|
12
|
+
from google.protobuf import wrappers_pb2
|
|
13
|
+
|
|
14
|
+
from durabletask.entities import EntityInstanceId
|
|
15
|
+
from durabletask.entities.entity_metadata import EntityMetadata
|
|
16
|
+
import durabletask.internal.helpers as helpers
|
|
17
|
+
import durabletask.internal.orchestrator_service_pb2 as pb
|
|
18
|
+
import durabletask.internal.orchestrator_service_pb2_grpc as stubs
|
|
19
|
+
import durabletask.internal.shared as shared
|
|
20
|
+
from durabletask import task
|
|
21
|
+
from durabletask.internal.grpc_interceptor import DefaultClientInterceptorImpl
|
|
22
|
+
|
|
23
|
+
TInput = TypeVar('TInput')
|
|
24
|
+
TOutput = TypeVar('TOutput')
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class OrchestrationStatus(Enum):
|
|
28
|
+
"""The status of an orchestration instance."""
|
|
29
|
+
RUNNING = pb.ORCHESTRATION_STATUS_RUNNING
|
|
30
|
+
COMPLETED = pb.ORCHESTRATION_STATUS_COMPLETED
|
|
31
|
+
FAILED = pb.ORCHESTRATION_STATUS_FAILED
|
|
32
|
+
TERMINATED = pb.ORCHESTRATION_STATUS_TERMINATED
|
|
33
|
+
CONTINUED_AS_NEW = pb.ORCHESTRATION_STATUS_CONTINUED_AS_NEW
|
|
34
|
+
PENDING = pb.ORCHESTRATION_STATUS_PENDING
|
|
35
|
+
SUSPENDED = pb.ORCHESTRATION_STATUS_SUSPENDED
|
|
36
|
+
|
|
37
|
+
def __str__(self):
|
|
38
|
+
return helpers.get_orchestration_status_str(self.value)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class OrchestrationState:
|
|
43
|
+
instance_id: str
|
|
44
|
+
name: str
|
|
45
|
+
runtime_status: OrchestrationStatus
|
|
46
|
+
created_at: datetime
|
|
47
|
+
last_updated_at: datetime
|
|
48
|
+
serialized_input: Optional[str]
|
|
49
|
+
serialized_output: Optional[str]
|
|
50
|
+
serialized_custom_status: Optional[str]
|
|
51
|
+
failure_details: Optional[task.FailureDetails]
|
|
52
|
+
|
|
53
|
+
def raise_if_failed(self):
|
|
54
|
+
if self.failure_details is not None:
|
|
55
|
+
raise OrchestrationFailedError(
|
|
56
|
+
f"Orchestration '{self.instance_id}' failed: {self.failure_details.message}",
|
|
57
|
+
self.failure_details)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class OrchestrationFailedError(Exception):
|
|
61
|
+
def __init__(self, message: str, failure_details: task.FailureDetails):
|
|
62
|
+
super().__init__(message)
|
|
63
|
+
self._failure_details = failure_details
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def failure_details(self):
|
|
67
|
+
return self._failure_details
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def new_orchestration_state(instance_id: str, res: pb.GetInstanceResponse) -> Optional[OrchestrationState]:
|
|
71
|
+
if not res.exists:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
state = res.orchestrationState
|
|
75
|
+
|
|
76
|
+
failure_details = None
|
|
77
|
+
if state.failureDetails.errorMessage != '' or state.failureDetails.errorType != '':
|
|
78
|
+
failure_details = task.FailureDetails(
|
|
79
|
+
state.failureDetails.errorMessage,
|
|
80
|
+
state.failureDetails.errorType,
|
|
81
|
+
state.failureDetails.stackTrace.value if not helpers.is_empty(state.failureDetails.stackTrace) else None)
|
|
82
|
+
|
|
83
|
+
return OrchestrationState(
|
|
84
|
+
instance_id,
|
|
85
|
+
state.name,
|
|
86
|
+
OrchestrationStatus(state.orchestrationStatus),
|
|
87
|
+
state.createdTimestamp.ToDatetime(),
|
|
88
|
+
state.lastUpdatedTimestamp.ToDatetime(),
|
|
89
|
+
state.input.value if not helpers.is_empty(state.input) else None,
|
|
90
|
+
state.output.value if not helpers.is_empty(state.output) else None,
|
|
91
|
+
state.customStatus.value if not helpers.is_empty(state.customStatus) else None,
|
|
92
|
+
failure_details)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class TaskHubGrpcClient:
|
|
96
|
+
|
|
97
|
+
def __init__(self, *,
|
|
98
|
+
host_address: Optional[str] = None,
|
|
99
|
+
metadata: Optional[list[tuple[str, str]]] = None,
|
|
100
|
+
log_handler: Optional[logging.Handler] = None,
|
|
101
|
+
log_formatter: Optional[logging.Formatter] = None,
|
|
102
|
+
secure_channel: bool = False,
|
|
103
|
+
interceptors: Optional[Sequence[shared.ClientInterceptor]] = None,
|
|
104
|
+
default_version: Optional[str] = None):
|
|
105
|
+
|
|
106
|
+
# If the caller provided metadata, we need to create a new interceptor for it and
|
|
107
|
+
# add it to the list of interceptors.
|
|
108
|
+
if interceptors is not None:
|
|
109
|
+
interceptors = list(interceptors)
|
|
110
|
+
if metadata is not None:
|
|
111
|
+
interceptors.append(DefaultClientInterceptorImpl(metadata))
|
|
112
|
+
elif metadata is not None:
|
|
113
|
+
interceptors = [DefaultClientInterceptorImpl(metadata)]
|
|
114
|
+
else:
|
|
115
|
+
interceptors = None
|
|
116
|
+
|
|
117
|
+
channel = shared.get_grpc_channel(
|
|
118
|
+
host_address=host_address,
|
|
119
|
+
secure_channel=secure_channel,
|
|
120
|
+
interceptors=interceptors
|
|
121
|
+
)
|
|
122
|
+
self._stub = stubs.TaskHubSidecarServiceStub(channel)
|
|
123
|
+
self._logger = shared.get_logger("client", log_handler, log_formatter)
|
|
124
|
+
self.default_version = default_version
|
|
125
|
+
|
|
126
|
+
def schedule_new_orchestration(self, orchestrator: Union[task.Orchestrator[TInput, TOutput], str], *,
|
|
127
|
+
input: Optional[TInput] = None,
|
|
128
|
+
instance_id: Optional[str] = None,
|
|
129
|
+
start_at: Optional[datetime] = None,
|
|
130
|
+
reuse_id_policy: Optional[pb.OrchestrationIdReusePolicy] = None,
|
|
131
|
+
tags: Optional[dict[str, str]] = None,
|
|
132
|
+
version: Optional[str] = None) -> str:
|
|
133
|
+
|
|
134
|
+
name = orchestrator if isinstance(orchestrator, str) else task.get_name(orchestrator)
|
|
135
|
+
|
|
136
|
+
req = pb.CreateInstanceRequest(
|
|
137
|
+
name=name,
|
|
138
|
+
instanceId=instance_id if instance_id else uuid.uuid4().hex,
|
|
139
|
+
input=wrappers_pb2.StringValue(value=shared.to_json(input)) if input is not None else None,
|
|
140
|
+
scheduledStartTimestamp=helpers.new_timestamp(start_at) if start_at else None,
|
|
141
|
+
version=helpers.get_string_value(version if version else self.default_version),
|
|
142
|
+
orchestrationIdReusePolicy=reuse_id_policy,
|
|
143
|
+
tags=tags
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
self._logger.info(f"Starting new '{name}' instance with ID = '{req.instanceId}'.")
|
|
147
|
+
res: pb.CreateInstanceResponse = self._stub.StartInstance(req)
|
|
148
|
+
return res.instanceId
|
|
149
|
+
|
|
150
|
+
def get_orchestration_state(self, instance_id: str, *, fetch_payloads: bool = True) -> Optional[OrchestrationState]:
|
|
151
|
+
req = pb.GetInstanceRequest(instanceId=instance_id, getInputsAndOutputs=fetch_payloads)
|
|
152
|
+
res: pb.GetInstanceResponse = self._stub.GetInstance(req)
|
|
153
|
+
return new_orchestration_state(req.instanceId, res)
|
|
154
|
+
|
|
155
|
+
def wait_for_orchestration_start(self, instance_id: str, *,
|
|
156
|
+
fetch_payloads: bool = False,
|
|
157
|
+
timeout: int = 60) -> Optional[OrchestrationState]:
|
|
158
|
+
req = pb.GetInstanceRequest(instanceId=instance_id, getInputsAndOutputs=fetch_payloads)
|
|
159
|
+
try:
|
|
160
|
+
self._logger.info(f"Waiting up to {timeout}s for instance '{instance_id}' to start.")
|
|
161
|
+
res: pb.GetInstanceResponse = self._stub.WaitForInstanceStart(req, timeout=timeout)
|
|
162
|
+
return new_orchestration_state(req.instanceId, res)
|
|
163
|
+
except grpc.RpcError as rpc_error:
|
|
164
|
+
if rpc_error.code() == grpc.StatusCode.DEADLINE_EXCEEDED: # type: ignore
|
|
165
|
+
# Replace gRPC error with the built-in TimeoutError
|
|
166
|
+
raise TimeoutError("Timed-out waiting for the orchestration to start")
|
|
167
|
+
else:
|
|
168
|
+
raise
|
|
169
|
+
|
|
170
|
+
def wait_for_orchestration_completion(self, instance_id: str, *,
|
|
171
|
+
fetch_payloads: bool = True,
|
|
172
|
+
timeout: int = 60) -> Optional[OrchestrationState]:
|
|
173
|
+
req = pb.GetInstanceRequest(instanceId=instance_id, getInputsAndOutputs=fetch_payloads)
|
|
174
|
+
try:
|
|
175
|
+
self._logger.info(f"Waiting {timeout}s for instance '{instance_id}' to complete.")
|
|
176
|
+
res: pb.GetInstanceResponse = self._stub.WaitForInstanceCompletion(req, timeout=timeout)
|
|
177
|
+
state = new_orchestration_state(req.instanceId, res)
|
|
178
|
+
if not state:
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
if state.runtime_status == OrchestrationStatus.FAILED and state.failure_details is not None:
|
|
182
|
+
details = state.failure_details
|
|
183
|
+
self._logger.info(f"Instance '{instance_id}' failed: [{details.error_type}] {details.message}")
|
|
184
|
+
elif state.runtime_status == OrchestrationStatus.TERMINATED:
|
|
185
|
+
self._logger.info(f"Instance '{instance_id}' was terminated.")
|
|
186
|
+
elif state.runtime_status == OrchestrationStatus.COMPLETED:
|
|
187
|
+
self._logger.info(f"Instance '{instance_id}' completed.")
|
|
188
|
+
|
|
189
|
+
return state
|
|
190
|
+
except grpc.RpcError as rpc_error:
|
|
191
|
+
if rpc_error.code() == grpc.StatusCode.DEADLINE_EXCEEDED: # type: ignore
|
|
192
|
+
# Replace gRPC error with the built-in TimeoutError
|
|
193
|
+
raise TimeoutError("Timed-out waiting for the orchestration to complete")
|
|
194
|
+
else:
|
|
195
|
+
raise
|
|
196
|
+
|
|
197
|
+
def raise_orchestration_event(self, instance_id: str, event_name: str, *,
|
|
198
|
+
data: Optional[Any] = None):
|
|
199
|
+
req = pb.RaiseEventRequest(
|
|
200
|
+
instanceId=instance_id,
|
|
201
|
+
name=event_name,
|
|
202
|
+
input=wrappers_pb2.StringValue(value=shared.to_json(data)) if data else None)
|
|
203
|
+
|
|
204
|
+
self._logger.info(f"Raising event '{event_name}' for instance '{instance_id}'.")
|
|
205
|
+
self._stub.RaiseEvent(req)
|
|
206
|
+
|
|
207
|
+
def terminate_orchestration(self, instance_id: str, *,
|
|
208
|
+
output: Optional[Any] = None,
|
|
209
|
+
recursive: bool = True):
|
|
210
|
+
req = pb.TerminateRequest(
|
|
211
|
+
instanceId=instance_id,
|
|
212
|
+
output=wrappers_pb2.StringValue(value=shared.to_json(output)) if output else None,
|
|
213
|
+
recursive=recursive)
|
|
214
|
+
|
|
215
|
+
self._logger.info(f"Terminating instance '{instance_id}'.")
|
|
216
|
+
self._stub.TerminateInstance(req)
|
|
217
|
+
|
|
218
|
+
def suspend_orchestration(self, instance_id: str):
|
|
219
|
+
req = pb.SuspendRequest(instanceId=instance_id)
|
|
220
|
+
self._logger.info(f"Suspending instance '{instance_id}'.")
|
|
221
|
+
self._stub.SuspendInstance(req)
|
|
222
|
+
|
|
223
|
+
def resume_orchestration(self, instance_id: str):
|
|
224
|
+
req = pb.ResumeRequest(instanceId=instance_id)
|
|
225
|
+
self._logger.info(f"Resuming instance '{instance_id}'.")
|
|
226
|
+
self._stub.ResumeInstance(req)
|
|
227
|
+
|
|
228
|
+
def purge_orchestration(self, instance_id: str, recursive: bool = True):
|
|
229
|
+
req = pb.PurgeInstancesRequest(instanceId=instance_id, recursive=recursive)
|
|
230
|
+
self._logger.info(f"Purging instance '{instance_id}'.")
|
|
231
|
+
self._stub.PurgeInstances(req)
|
|
232
|
+
|
|
233
|
+
def signal_entity(self, entity_instance_id: EntityInstanceId, operation_name: str, input: Optional[Any] = None):
|
|
234
|
+
req = pb.SignalEntityRequest(
|
|
235
|
+
instanceId=str(entity_instance_id),
|
|
236
|
+
name=operation_name,
|
|
237
|
+
input=wrappers_pb2.StringValue(value=shared.to_json(input)) if input else None,
|
|
238
|
+
requestId=str(uuid.uuid4()),
|
|
239
|
+
scheduledTime=None,
|
|
240
|
+
parentTraceContext=None,
|
|
241
|
+
requestTime=helpers.new_timestamp(datetime.now(timezone.utc))
|
|
242
|
+
)
|
|
243
|
+
self._logger.info(f"Signaling entity '{entity_instance_id}' operation '{operation_name}'.")
|
|
244
|
+
self._stub.SignalEntity(req, None) # TODO: Cancellation timeout?
|
|
245
|
+
|
|
246
|
+
def get_entity(self,
|
|
247
|
+
entity_instance_id: EntityInstanceId,
|
|
248
|
+
include_state: bool = True
|
|
249
|
+
) -> Optional[EntityMetadata]:
|
|
250
|
+
req = pb.GetEntityRequest(instanceId=str(entity_instance_id), includeState=include_state)
|
|
251
|
+
self._logger.info(f"Getting entity '{entity_instance_id}'.")
|
|
252
|
+
res: pb.GetEntityResponse = self._stub.GetEntity(req)
|
|
253
|
+
if not res.exists:
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
return EntityMetadata.from_entity_response(res, include_state)
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
from durabletask.entities.entity_metadata import EntityMetadata
|
|
11
|
+
|
|
12
|
+
__all__ = ["EntityInstanceId", "DurableEntity", "EntityLock", "EntityContext", "EntityMetadata"]
|
|
13
|
+
|
|
14
|
+
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) -> "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(f"Invalid entity ID format: {entity_id}", ex)
|