durabletask 0.0.0.dev1__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.
@@ -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"
durabletask/client.py ADDED
@@ -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)
@@ -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,97 @@
1
+ from datetime import datetime, timezone
2
+ from typing import Any, Optional, Type, TypeVar, Union, overload
3
+ from durabletask.entities.entity_instance_id import EntityInstanceId
4
+
5
+ import durabletask.internal.orchestrator_service_pb2 as pb
6
+
7
+ TState = TypeVar("TState")
8
+
9
+
10
+ class EntityMetadata:
11
+ """Class representing the metadata of a durable entity.
12
+
13
+ This class encapsulates the metadata information of a durable entity, allowing for
14
+ easy access and manipulation of the entity's metadata within the Durable Task
15
+ Framework.
16
+
17
+ Attributes:
18
+ id (EntityInstanceId): The unique identifier of the entity instance.
19
+ last_modified (datetime): The timestamp of the last modification to the entity.
20
+ backlog_queue_size (int): The size of the backlog queue for the entity.
21
+ locked_by (str): The identifier of the worker that currently holds the lock on the entity.
22
+ includes_state (bool): Indicates whether the metadata includes the state of the entity.
23
+ state (Optional[Any]): The current state of the entity, if included.
24
+ """
25
+
26
+ def __init__(self,
27
+ id: EntityInstanceId,
28
+ last_modified: datetime,
29
+ backlog_queue_size: int,
30
+ locked_by: str,
31
+ includes_state: bool,
32
+ state: Optional[Any]):
33
+ """Initializes a new instance of the EntityMetadata class.
34
+
35
+ Args:
36
+ value: The initial state value of the entity.
37
+ """
38
+ self.id = id
39
+ self.last_modified = last_modified
40
+ self.backlog_queue_size = backlog_queue_size
41
+ self._locked_by = locked_by
42
+ self.includes_state = includes_state
43
+ self._state = state
44
+
45
+ @staticmethod
46
+ def from_entity_response(entity_response: pb.GetEntityResponse, includes_state: bool):
47
+ entity_id = EntityInstanceId.parse(entity_response.entity.instanceId)
48
+ if not entity_id:
49
+ raise ValueError("Invalid entity instance ID in entity response.")
50
+ entity_state = None
51
+ if includes_state:
52
+ entity_state = entity_response.entity.serializedState.value
53
+ return EntityMetadata(
54
+ id=entity_id,
55
+ last_modified=entity_response.entity.lastModifiedTime.ToDatetime(timezone.utc),
56
+ backlog_queue_size=entity_response.entity.backlogQueueSize,
57
+ locked_by=entity_response.entity.lockedBy.value,
58
+ includes_state=includes_state,
59
+ state=entity_state
60
+ )
61
+
62
+ @overload
63
+ def get_state(self, intended_type: Type[TState]) -> Optional[TState]:
64
+ ...
65
+
66
+ @overload
67
+ def get_state(self, intended_type: None = None) -> Any:
68
+ ...
69
+
70
+ def get_state(self, intended_type: Optional[Type[TState]] = None) -> Union[None, TState, Any]:
71
+ """Get the current state of the entity, optionally converting it to a specified type."""
72
+ if intended_type is None or self._state is None:
73
+ return self._state
74
+
75
+ if isinstance(self._state, intended_type):
76
+ return self._state
77
+
78
+ try:
79
+ return intended_type(self._state) # type: ignore[call-arg]
80
+ except Exception as ex:
81
+ raise TypeError(
82
+ f"Could not convert state of type '{type(self._state).__name__}' to '{intended_type.__name__}'"
83
+ ) from ex
84
+
85
+ def get_locked_by(self) -> Optional[EntityInstanceId]:
86
+ """Get the identifier of the worker that currently holds the lock on the entity.
87
+
88
+ Returns
89
+ -------
90
+ str
91
+ The identifier of the worker that currently holds the lock on the entity.
92
+ """
93
+ if not self._locked_by:
94
+ return None
95
+
96
+ # Will throw ValueError if the format is invalid
97
+ return EntityInstanceId.parse(self._locked_by)