durabletask 0.0.0.dev38__tar.gz → 0.0.0.dev40__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.dev38 → durabletask-0.0.0.dev40}/PKG-INFO +1 -1
- {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/client.py +137 -32
- {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/entities/__init__.py +3 -1
- durabletask-0.0.0.dev40/durabletask/entities/entity_instance_id.py +84 -0
- {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/entities/entity_metadata.py +9 -5
- durabletask-0.0.0.dev40/durabletask/entities/entity_operation_failed_exception.py +15 -0
- {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/internal/helpers.py +7 -0
- durabletask-0.0.0.dev40/durabletask/internal/json_encode_output_exception.py +12 -0
- durabletask-0.0.0.dev40/durabletask/internal/orchestrator_service_pb2.py +270 -0
- {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/internal/orchestrator_service_pb2.pyi +41 -208
- {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/internal/orchestrator_service_pb2_grpc.py +8 -135
- {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/internal/proto_task_hub_sidecar_service_stub.py +0 -1
- {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/worker.py +41 -19
- {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask.egg-info/PKG-INFO +1 -1
- {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask.egg-info/SOURCES.txt +2 -0
- {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/pyproject.toml +1 -1
- durabletask-0.0.0.dev38/durabletask/entities/entity_instance_id.py +0 -42
- durabletask-0.0.0.dev38/durabletask/internal/orchestrator_service_pb2.py +0 -322
- {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/LICENSE +0 -0
- {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/README.md +0 -0
- {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/__init__.py +0 -0
- {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/entities/durable_entity.py +0 -0
- {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/entities/entity_context.py +0 -0
- {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/entities/entity_lock.py +0 -0
- {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/internal/entity_state_shim.py +0 -0
- {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/internal/exceptions.py +0 -0
- {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/internal/grpc_interceptor.py +0 -0
- {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/internal/orchestration_entity_context.py +0 -0
- {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/internal/shared.py +0 -0
- {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/py.typed +0 -0
- {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/task.py +0 -0
- {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask.egg-info/dependency_links.txt +0 -0
- {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask.egg-info/requires.txt +0 -0
- {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask.egg-info/top_level.txt +0 -0
- {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/setup.cfg +0 -0
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
import logging
|
|
5
5
|
import uuid
|
|
6
6
|
from dataclasses import dataclass
|
|
7
|
-
from datetime import datetime, timezone
|
|
7
|
+
from datetime import datetime, timedelta, timezone
|
|
8
8
|
from enum import Enum
|
|
9
|
-
from typing import Any, Optional, Sequence, TypeVar, Union
|
|
9
|
+
from typing import Any, List, Optional, Sequence, TypeVar, Union
|
|
10
10
|
|
|
11
11
|
import grpc
|
|
12
|
-
from google.protobuf import wrappers_pb2
|
|
12
|
+
from google.protobuf import wrappers_pb2 as pb2
|
|
13
13
|
|
|
14
14
|
from durabletask.entities import EntityInstanceId
|
|
15
15
|
from durabletask.entities.entity_metadata import EntityMetadata
|
|
@@ -57,6 +57,12 @@ class OrchestrationState:
|
|
|
57
57
|
self.failure_details)
|
|
58
58
|
|
|
59
59
|
|
|
60
|
+
class PurgeInstancesResult:
|
|
61
|
+
def __init__(self, deleted_instance_count: int, is_complete: bool):
|
|
62
|
+
self.deleted_instance_count = deleted_instance_count
|
|
63
|
+
self.is_complete = is_complete
|
|
64
|
+
|
|
65
|
+
|
|
60
66
|
class OrchestrationFailedError(Exception):
|
|
61
67
|
def __init__(self, message: str, failure_details: task.FailureDetails):
|
|
62
68
|
super().__init__(message)
|
|
@@ -73,6 +79,12 @@ def new_orchestration_state(instance_id: str, res: pb.GetInstanceResponse) -> Op
|
|
|
73
79
|
|
|
74
80
|
state = res.orchestrationState
|
|
75
81
|
|
|
82
|
+
new_state = parse_orchestration_state(state)
|
|
83
|
+
new_state.instance_id = instance_id # Override instance_id with the one from the request, to match old behavior
|
|
84
|
+
return new_state
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def parse_orchestration_state(state: pb.OrchestrationState) -> OrchestrationState:
|
|
76
88
|
failure_details = None
|
|
77
89
|
if state.failureDetails.errorMessage != '' or state.failureDetails.errorType != '':
|
|
78
90
|
failure_details = task.FailureDetails(
|
|
@@ -81,7 +93,7 @@ def new_orchestration_state(instance_id: str, res: pb.GetInstanceResponse) -> Op
|
|
|
81
93
|
state.failureDetails.stackTrace.value if not helpers.is_empty(state.failureDetails.stackTrace) else None)
|
|
82
94
|
|
|
83
95
|
return OrchestrationState(
|
|
84
|
-
|
|
96
|
+
state.instanceId,
|
|
85
97
|
state.name,
|
|
86
98
|
OrchestrationStatus(state.orchestrationStatus),
|
|
87
99
|
state.createdTimestamp.ToDatetime(),
|
|
@@ -93,7 +105,6 @@ def new_orchestration_state(instance_id: str, res: pb.GetInstanceResponse) -> Op
|
|
|
93
105
|
|
|
94
106
|
|
|
95
107
|
class TaskHubGrpcClient:
|
|
96
|
-
|
|
97
108
|
def __init__(self, *,
|
|
98
109
|
host_address: Optional[str] = None,
|
|
99
110
|
metadata: Optional[list[tuple[str, str]]] = None,
|
|
@@ -136,7 +147,7 @@ class TaskHubGrpcClient:
|
|
|
136
147
|
req = pb.CreateInstanceRequest(
|
|
137
148
|
name=name,
|
|
138
149
|
instanceId=instance_id if instance_id else uuid.uuid4().hex,
|
|
139
|
-
input=
|
|
150
|
+
input=helpers.get_string_value(shared.to_json(input)),
|
|
140
151
|
scheduledStartTimestamp=helpers.new_timestamp(start_at) if start_at else None,
|
|
141
152
|
version=helpers.get_string_value(version if version else self.default_version),
|
|
142
153
|
orchestrationIdReusePolicy=reuse_id_policy,
|
|
@@ -152,6 +163,54 @@ class TaskHubGrpcClient:
|
|
|
152
163
|
res: pb.GetInstanceResponse = self._stub.GetInstance(req)
|
|
153
164
|
return new_orchestration_state(req.instanceId, res)
|
|
154
165
|
|
|
166
|
+
def get_all_orchestration_states(self,
|
|
167
|
+
max_instance_count: Optional[int] = None,
|
|
168
|
+
fetch_inputs_and_outputs: bool = False) -> List[OrchestrationState]:
|
|
169
|
+
return self.get_orchestration_state_by(
|
|
170
|
+
created_time_from=None,
|
|
171
|
+
created_time_to=None,
|
|
172
|
+
runtime_status=None,
|
|
173
|
+
max_instance_count=max_instance_count,
|
|
174
|
+
fetch_inputs_and_outputs=fetch_inputs_and_outputs
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def get_orchestration_state_by(self,
|
|
178
|
+
created_time_from: Optional[datetime] = None,
|
|
179
|
+
created_time_to: Optional[datetime] = None,
|
|
180
|
+
runtime_status: Optional[List[OrchestrationStatus]] = None,
|
|
181
|
+
max_instance_count: Optional[int] = None,
|
|
182
|
+
fetch_inputs_and_outputs: bool = False,
|
|
183
|
+
_continuation_token: Optional[pb2.StringValue] = None
|
|
184
|
+
) -> List[OrchestrationState]:
|
|
185
|
+
if max_instance_count is None:
|
|
186
|
+
# DTS backend does not behave well with max_instance_count = None, so we set to max 32-bit signed value
|
|
187
|
+
max_instance_count = (1 << 31) - 1
|
|
188
|
+
req = pb.QueryInstancesRequest(
|
|
189
|
+
query=pb.InstanceQuery(
|
|
190
|
+
runtimeStatus=[status.value for status in runtime_status] if runtime_status else None,
|
|
191
|
+
createdTimeFrom=helpers.new_timestamp(created_time_from) if created_time_from else None,
|
|
192
|
+
createdTimeTo=helpers.new_timestamp(created_time_to) if created_time_to else None,
|
|
193
|
+
maxInstanceCount=max_instance_count,
|
|
194
|
+
fetchInputsAndOutputs=fetch_inputs_and_outputs,
|
|
195
|
+
continuationToken=_continuation_token
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
resp: pb.QueryInstancesResponse = self._stub.QueryInstances(req)
|
|
199
|
+
states = [parse_orchestration_state(res) for res in resp.orchestrationState]
|
|
200
|
+
# Check the value for continuationToken - none or "0" indicates that there are no more results.
|
|
201
|
+
if resp.continuationToken and resp.continuationToken.value and resp.continuationToken.value != "0":
|
|
202
|
+
self._logger.info(f"Received continuation token with value {resp.continuationToken.value}, fetching next list of instances...")
|
|
203
|
+
states += self.get_orchestration_state_by(
|
|
204
|
+
created_time_from,
|
|
205
|
+
created_time_to,
|
|
206
|
+
runtime_status,
|
|
207
|
+
max_instance_count,
|
|
208
|
+
fetch_inputs_and_outputs,
|
|
209
|
+
_continuation_token=resp.continuationToken
|
|
210
|
+
)
|
|
211
|
+
states = [state for state in states if state is not None] # Filter out any None values
|
|
212
|
+
return states
|
|
213
|
+
|
|
155
214
|
def wait_for_orchestration_start(self, instance_id: str, *,
|
|
156
215
|
fetch_payloads: bool = False,
|
|
157
216
|
timeout: int = 60) -> Optional[OrchestrationState]:
|
|
@@ -199,7 +258,7 @@ class TaskHubGrpcClient:
|
|
|
199
258
|
req = pb.RaiseEventRequest(
|
|
200
259
|
instanceId=instance_id,
|
|
201
260
|
name=event_name,
|
|
202
|
-
input=
|
|
261
|
+
input=helpers.get_string_value(shared.to_json(data)))
|
|
203
262
|
|
|
204
263
|
self._logger.info(f"Raising event '{event_name}' for instance '{instance_id}'.")
|
|
205
264
|
self._stub.RaiseEvent(req)
|
|
@@ -209,7 +268,7 @@ class TaskHubGrpcClient:
|
|
|
209
268
|
recursive: bool = True):
|
|
210
269
|
req = pb.TerminateRequest(
|
|
211
270
|
instanceId=instance_id,
|
|
212
|
-
output=
|
|
271
|
+
output=helpers.get_string_value(shared.to_json(output)),
|
|
213
272
|
recursive=recursive)
|
|
214
273
|
|
|
215
274
|
self._logger.info(f"Terminating instance '{instance_id}'.")
|
|
@@ -225,30 +284,27 @@ class TaskHubGrpcClient:
|
|
|
225
284
|
self._logger.info(f"Resuming instance '{instance_id}'.")
|
|
226
285
|
self._stub.ResumeInstance(req)
|
|
227
286
|
|
|
228
|
-
def
|
|
229
|
-
restart_with_new_instance_id: bool = False) -> str:
|
|
230
|
-
"""Restarts an existing orchestration instance.
|
|
231
|
-
|
|
232
|
-
Args:
|
|
233
|
-
instance_id: The ID of the orchestration instance to restart.
|
|
234
|
-
restart_with_new_instance_id: If True, the restarted orchestration will use a new instance ID.
|
|
235
|
-
If False (default), the restarted orchestration will reuse the same instance ID.
|
|
236
|
-
|
|
237
|
-
Returns:
|
|
238
|
-
The instance ID of the restarted orchestration.
|
|
239
|
-
"""
|
|
240
|
-
req = pb.RestartInstanceRequest(
|
|
241
|
-
instanceId=instance_id,
|
|
242
|
-
restartWithNewInstanceId=restart_with_new_instance_id)
|
|
243
|
-
|
|
244
|
-
self._logger.info(f"Restarting instance '{instance_id}'.")
|
|
245
|
-
res: pb.RestartInstanceResponse = self._stub.RestartInstance(req)
|
|
246
|
-
return res.instanceId
|
|
247
|
-
|
|
248
|
-
def purge_orchestration(self, instance_id: str, recursive: bool = True):
|
|
287
|
+
def purge_orchestration(self, instance_id: str, recursive: bool = True) -> PurgeInstancesResult:
|
|
249
288
|
req = pb.PurgeInstancesRequest(instanceId=instance_id, recursive=recursive)
|
|
250
289
|
self._logger.info(f"Purging instance '{instance_id}'.")
|
|
251
|
-
self._stub.PurgeInstances(req)
|
|
290
|
+
resp: pb.PurgeInstancesResponse = self._stub.PurgeInstances(req)
|
|
291
|
+
return PurgeInstancesResult(resp.deletedInstanceCount, resp.isComplete.value)
|
|
292
|
+
|
|
293
|
+
def purge_orchestrations_by(self,
|
|
294
|
+
created_time_from: Optional[datetime] = None,
|
|
295
|
+
created_time_to: Optional[datetime] = None,
|
|
296
|
+
runtime_status: Optional[List[OrchestrationStatus]] = None,
|
|
297
|
+
recursive: bool = False) -> PurgeInstancesResult:
|
|
298
|
+
resp: pb.PurgeInstancesResponse = self._stub.PurgeInstances(pb.PurgeInstancesRequest(
|
|
299
|
+
instanceId=None,
|
|
300
|
+
purgeInstanceFilter=pb.PurgeInstanceFilter(
|
|
301
|
+
createdTimeFrom=helpers.new_timestamp(created_time_from) if created_time_from else None,
|
|
302
|
+
createdTimeTo=helpers.new_timestamp(created_time_to) if created_time_to else None,
|
|
303
|
+
runtimeStatus=[status.value for status in runtime_status] if runtime_status else None
|
|
304
|
+
),
|
|
305
|
+
recursive=recursive
|
|
306
|
+
))
|
|
307
|
+
return PurgeInstancesResult(resp.deletedInstanceCount, resp.isComplete.value)
|
|
252
308
|
|
|
253
309
|
def signal_entity(self,
|
|
254
310
|
entity_instance_id: EntityInstanceId,
|
|
@@ -257,7 +313,7 @@ class TaskHubGrpcClient:
|
|
|
257
313
|
req = pb.SignalEntityRequest(
|
|
258
314
|
instanceId=str(entity_instance_id),
|
|
259
315
|
name=operation_name,
|
|
260
|
-
input=
|
|
316
|
+
input=helpers.get_string_value(shared.to_json(input)),
|
|
261
317
|
requestId=str(uuid.uuid4()),
|
|
262
318
|
scheduledTime=None,
|
|
263
319
|
parentTraceContext=None,
|
|
@@ -276,4 +332,53 @@ class TaskHubGrpcClient:
|
|
|
276
332
|
if not res.exists:
|
|
277
333
|
return None
|
|
278
334
|
|
|
279
|
-
return EntityMetadata.
|
|
335
|
+
return EntityMetadata.from_entity_metadata(res.entity, include_state)
|
|
336
|
+
|
|
337
|
+
def get_all_entities(self,
|
|
338
|
+
include_state: bool = True,
|
|
339
|
+
include_transient: bool = False,
|
|
340
|
+
page_size: Optional[int] = None) -> List[EntityMetadata]:
|
|
341
|
+
return self.get_entities_by(
|
|
342
|
+
instance_id_starts_with=None,
|
|
343
|
+
last_modified_from=None,
|
|
344
|
+
last_modified_to=None,
|
|
345
|
+
include_state=include_state,
|
|
346
|
+
include_transient=include_transient,
|
|
347
|
+
page_size=page_size
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
def get_entities_by(self,
|
|
351
|
+
instance_id_starts_with: Optional[str] = None,
|
|
352
|
+
last_modified_from: Optional[datetime] = None,
|
|
353
|
+
last_modified_to: Optional[datetime] = None,
|
|
354
|
+
include_state: bool = True,
|
|
355
|
+
include_transient: bool = False,
|
|
356
|
+
page_size: Optional[int] = None,
|
|
357
|
+
_continuation_token: Optional[pb2.StringValue] = None
|
|
358
|
+
) -> List[EntityMetadata]:
|
|
359
|
+
self._logger.info(f"Getting entities")
|
|
360
|
+
query_request = pb.QueryEntitiesRequest(
|
|
361
|
+
query=pb.EntityQuery(
|
|
362
|
+
instanceIdStartsWith=helpers.get_string_value(instance_id_starts_with),
|
|
363
|
+
lastModifiedFrom=helpers.new_timestamp(last_modified_from) if last_modified_from else None,
|
|
364
|
+
lastModifiedTo=helpers.new_timestamp(last_modified_to) if last_modified_to else None,
|
|
365
|
+
includeState=include_state,
|
|
366
|
+
includeTransient=include_transient,
|
|
367
|
+
pageSize=helpers.get_int_value(page_size),
|
|
368
|
+
continuationToken=_continuation_token
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
resp: pb.QueryEntitiesResponse = self._stub.QueryEntities(query_request)
|
|
372
|
+
entities = [EntityMetadata.from_entity_metadata(entity, query_request.query.includeState) for entity in resp.entities]
|
|
373
|
+
if resp.continuationToken and resp.continuationToken.value != "0":
|
|
374
|
+
self._logger.info(f"Received continuation token with value {resp.continuationToken.value}, fetching next page of entities...")
|
|
375
|
+
entities += self.get_entities_by(
|
|
376
|
+
instance_id_starts_with=instance_id_starts_with,
|
|
377
|
+
last_modified_from=last_modified_from,
|
|
378
|
+
last_modified_to=last_modified_to,
|
|
379
|
+
include_state=include_state,
|
|
380
|
+
include_transient=include_transient,
|
|
381
|
+
page_size=page_size,
|
|
382
|
+
_continuation_token=resp.continuationToken
|
|
383
|
+
)
|
|
384
|
+
return entities
|
|
@@ -8,7 +8,9 @@ from durabletask.entities.durable_entity import DurableEntity
|
|
|
8
8
|
from durabletask.entities.entity_lock import EntityLock
|
|
9
9
|
from durabletask.entities.entity_context import EntityContext
|
|
10
10
|
from durabletask.entities.entity_metadata import EntityMetadata
|
|
11
|
+
from durabletask.entities.entity_operation_failed_exception import EntityOperationFailedException
|
|
11
12
|
|
|
12
|
-
__all__ = ["EntityInstanceId", "DurableEntity", "EntityLock", "EntityContext", "EntityMetadata"
|
|
13
|
+
__all__ = ["EntityInstanceId", "DurableEntity", "EntityLock", "EntityContext", "EntityMetadata",
|
|
14
|
+
"EntityOperationFailedException"]
|
|
13
15
|
|
|
14
16
|
PACKAGE_NAME = "durabletask.entities"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
class EntityInstanceId:
|
|
2
|
+
def __init__(self, entity: str, key: str):
|
|
3
|
+
EntityInstanceId.validate_entity_name(entity)
|
|
4
|
+
EntityInstanceId.validate_key(key)
|
|
5
|
+
self.entity = entity.lower()
|
|
6
|
+
self.key = key
|
|
7
|
+
|
|
8
|
+
def __str__(self) -> str:
|
|
9
|
+
return f"@{self.entity}@{self.key}"
|
|
10
|
+
|
|
11
|
+
def __eq__(self, other):
|
|
12
|
+
if not isinstance(other, EntityInstanceId):
|
|
13
|
+
return False
|
|
14
|
+
return self.entity == other.entity and self.key == other.key
|
|
15
|
+
|
|
16
|
+
def __lt__(self, other):
|
|
17
|
+
if not isinstance(other, EntityInstanceId):
|
|
18
|
+
return self < other
|
|
19
|
+
return str(self) < str(other)
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def parse(entity_id: str) -> "EntityInstanceId":
|
|
23
|
+
"""Parse a string representation of an entity ID into an EntityInstanceId object.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
entity_id : str
|
|
28
|
+
The string representation of the entity ID, in the format '@entity@key'.
|
|
29
|
+
|
|
30
|
+
Returns
|
|
31
|
+
-------
|
|
32
|
+
EntityInstanceId
|
|
33
|
+
The parsed EntityInstanceId object.
|
|
34
|
+
|
|
35
|
+
Raises
|
|
36
|
+
------
|
|
37
|
+
ValueError
|
|
38
|
+
If the input string is not in the correct format.
|
|
39
|
+
"""
|
|
40
|
+
if not entity_id.startswith("@"):
|
|
41
|
+
raise ValueError("Entity ID must start with '@'.")
|
|
42
|
+
try:
|
|
43
|
+
_, entity, key = entity_id.split("@", 2)
|
|
44
|
+
except ValueError as ex:
|
|
45
|
+
raise ValueError(f"Invalid entity ID format: {entity_id}") from ex
|
|
46
|
+
return EntityInstanceId(entity=entity, key=key)
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def validate_entity_name(name: str) -> None:
|
|
50
|
+
"""Validate that the entity name does not contain invalid characters.
|
|
51
|
+
|
|
52
|
+
Parameters
|
|
53
|
+
----------
|
|
54
|
+
name : str
|
|
55
|
+
The entity name to validate.
|
|
56
|
+
|
|
57
|
+
Raises
|
|
58
|
+
------
|
|
59
|
+
ValueError
|
|
60
|
+
If the name is not a valid entity name.
|
|
61
|
+
"""
|
|
62
|
+
if not name:
|
|
63
|
+
raise ValueError("Entity name cannot be empty.")
|
|
64
|
+
if "@" in name:
|
|
65
|
+
raise ValueError("Entity name cannot contain '@' symbol.")
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def validate_key(key: str) -> None:
|
|
69
|
+
"""Validate that the entity key does not contain invalid characters.
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
key : str
|
|
74
|
+
The entity key to validate.
|
|
75
|
+
|
|
76
|
+
Raises
|
|
77
|
+
------
|
|
78
|
+
ValueError
|
|
79
|
+
If the key is not a valid entity key.
|
|
80
|
+
"""
|
|
81
|
+
if not key:
|
|
82
|
+
raise ValueError("Entity key cannot be empty.")
|
|
83
|
+
if "@" in key:
|
|
84
|
+
raise ValueError("Entity key cannot contain '@' symbol.")
|
|
@@ -44,18 +44,22 @@ class EntityMetadata:
|
|
|
44
44
|
|
|
45
45
|
@staticmethod
|
|
46
46
|
def from_entity_response(entity_response: pb.GetEntityResponse, includes_state: bool):
|
|
47
|
+
return EntityMetadata.from_entity_metadata(entity_response.entity, includes_state)
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def from_entity_metadata(entity: pb.EntityMetadata, includes_state: bool):
|
|
47
51
|
try:
|
|
48
|
-
entity_id = EntityInstanceId.parse(
|
|
52
|
+
entity_id = EntityInstanceId.parse(entity.instanceId)
|
|
49
53
|
except ValueError:
|
|
50
54
|
raise ValueError("Invalid entity instance ID in entity response.")
|
|
51
55
|
entity_state = None
|
|
52
56
|
if includes_state:
|
|
53
|
-
entity_state =
|
|
57
|
+
entity_state = entity.serializedState.value
|
|
54
58
|
return EntityMetadata(
|
|
55
59
|
id=entity_id,
|
|
56
|
-
last_modified=
|
|
57
|
-
backlog_queue_size=
|
|
58
|
-
locked_by=
|
|
60
|
+
last_modified=entity.lastModifiedTime.ToDatetime(timezone.utc),
|
|
61
|
+
backlog_queue_size=entity.backlogQueueSize,
|
|
62
|
+
locked_by=entity.lockedBy.value,
|
|
59
63
|
includes_state=includes_state,
|
|
60
64
|
state=entity_state
|
|
61
65
|
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from durabletask.internal.orchestrator_service_pb2 import TaskFailureDetails
|
|
2
|
+
from durabletask.entities.entity_instance_id import EntityInstanceId
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class EntityOperationFailedException(Exception):
|
|
6
|
+
"""Exception raised when an operation on an Entity Function fails."""
|
|
7
|
+
|
|
8
|
+
def __init__(self, entity_instance_id: EntityInstanceId, operation_name: str, failure_details: TaskFailureDetails) -> None:
|
|
9
|
+
super().__init__()
|
|
10
|
+
self.entity_instance_id = entity_instance_id
|
|
11
|
+
self.operation_name = operation_name
|
|
12
|
+
self.failure_details = failure_details
|
|
13
|
+
|
|
14
|
+
def __str__(self) -> str:
|
|
15
|
+
return f"Operation '{self.operation_name}' on entity '{self.entity_instance_id}' failed with error: {self.failure_details.errorMessage}"
|
|
@@ -184,6 +184,13 @@ def get_string_value(val: Optional[str]) -> Optional[wrappers_pb2.StringValue]:
|
|
|
184
184
|
return wrappers_pb2.StringValue(value=val)
|
|
185
185
|
|
|
186
186
|
|
|
187
|
+
def get_int_value(val: Optional[int]) -> Optional[wrappers_pb2.Int32Value]:
|
|
188
|
+
if val is None:
|
|
189
|
+
return None
|
|
190
|
+
else:
|
|
191
|
+
return wrappers_pb2.Int32Value(value=val)
|
|
192
|
+
|
|
193
|
+
|
|
187
194
|
def get_string_value_or_empty(val: Optional[str]) -> wrappers_pb2.StringValue:
|
|
188
195
|
if val is None:
|
|
189
196
|
return wrappers_pb2.StringValue(value="")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class JsonEncodeOutputException(Exception):
|
|
5
|
+
"""Custom exception type used to indicate that an orchestration result could not be JSON-encoded."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, problem_object: Any):
|
|
8
|
+
super().__init__()
|
|
9
|
+
self.problem_object = problem_object
|
|
10
|
+
|
|
11
|
+
def __str__(self) -> str:
|
|
12
|
+
return f"The orchestration result could not be encoded. Object details: {self.problem_object}"
|