durabletask 0.0.0.dev63__tar.gz → 0.0.0.dev64__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.
Files changed (35) hide show
  1. {durabletask-0.0.0.dev63 → durabletask-0.0.0.dev64}/PKG-INFO +1 -1
  2. {durabletask-0.0.0.dev63 → durabletask-0.0.0.dev64}/durabletask/client.py +11 -191
  3. {durabletask-0.0.0.dev63 → durabletask-0.0.0.dev64}/durabletask/entities/__init__.py +1 -3
  4. durabletask-0.0.0.dev64/durabletask/entities/entity_instance_id.py +42 -0
  5. {durabletask-0.0.0.dev63 → durabletask-0.0.0.dev64}/durabletask/entities/entity_metadata.py +5 -9
  6. {durabletask-0.0.0.dev63 → durabletask-0.0.0.dev64}/durabletask/internal/helpers.py +0 -7
  7. durabletask-0.0.0.dev64/durabletask/internal/orchestrator_service_pb2.py +270 -0
  8. {durabletask-0.0.0.dev63 → durabletask-0.0.0.dev64}/durabletask/internal/orchestrator_service_pb2.pyi +22 -186
  9. {durabletask-0.0.0.dev63 → durabletask-0.0.0.dev64}/durabletask/internal/orchestrator_service_pb2_grpc.py +0 -132
  10. {durabletask-0.0.0.dev63 → durabletask-0.0.0.dev64}/durabletask/internal/proto_task_hub_sidecar_service_stub.py +0 -3
  11. {durabletask-0.0.0.dev63 → durabletask-0.0.0.dev64}/durabletask/worker.py +29 -67
  12. {durabletask-0.0.0.dev63 → durabletask-0.0.0.dev64}/durabletask.egg-info/PKG-INFO +1 -1
  13. {durabletask-0.0.0.dev63 → durabletask-0.0.0.dev64}/durabletask.egg-info/SOURCES.txt +0 -2
  14. {durabletask-0.0.0.dev63 → durabletask-0.0.0.dev64}/pyproject.toml +1 -1
  15. durabletask-0.0.0.dev63/durabletask/entities/entity_instance_id.py +0 -84
  16. durabletask-0.0.0.dev63/durabletask/entities/entity_operation_failed_exception.py +0 -15
  17. durabletask-0.0.0.dev63/durabletask/internal/json_encode_output_exception.py +0 -12
  18. durabletask-0.0.0.dev63/durabletask/internal/orchestrator_service_pb2.py +0 -312
  19. {durabletask-0.0.0.dev63 → durabletask-0.0.0.dev64}/LICENSE +0 -0
  20. {durabletask-0.0.0.dev63 → durabletask-0.0.0.dev64}/README.md +0 -0
  21. {durabletask-0.0.0.dev63 → durabletask-0.0.0.dev64}/durabletask/__init__.py +0 -0
  22. {durabletask-0.0.0.dev63 → durabletask-0.0.0.dev64}/durabletask/entities/durable_entity.py +0 -0
  23. {durabletask-0.0.0.dev63 → durabletask-0.0.0.dev64}/durabletask/entities/entity_context.py +0 -0
  24. {durabletask-0.0.0.dev63 → durabletask-0.0.0.dev64}/durabletask/entities/entity_lock.py +0 -0
  25. {durabletask-0.0.0.dev63 → durabletask-0.0.0.dev64}/durabletask/internal/entity_state_shim.py +0 -0
  26. {durabletask-0.0.0.dev63 → durabletask-0.0.0.dev64}/durabletask/internal/exceptions.py +0 -0
  27. {durabletask-0.0.0.dev63 → durabletask-0.0.0.dev64}/durabletask/internal/grpc_interceptor.py +0 -0
  28. {durabletask-0.0.0.dev63 → durabletask-0.0.0.dev64}/durabletask/internal/orchestration_entity_context.py +0 -0
  29. {durabletask-0.0.0.dev63 → durabletask-0.0.0.dev64}/durabletask/internal/shared.py +0 -0
  30. {durabletask-0.0.0.dev63 → durabletask-0.0.0.dev64}/durabletask/py.typed +0 -0
  31. {durabletask-0.0.0.dev63 → durabletask-0.0.0.dev64}/durabletask/task.py +0 -0
  32. {durabletask-0.0.0.dev63 → durabletask-0.0.0.dev64}/durabletask.egg-info/dependency_links.txt +0 -0
  33. {durabletask-0.0.0.dev63 → durabletask-0.0.0.dev64}/durabletask.egg-info/requires.txt +0 -0
  34. {durabletask-0.0.0.dev63 → durabletask-0.0.0.dev64}/durabletask.egg-info/top_level.txt +0 -0
  35. {durabletask-0.0.0.dev63 → durabletask-0.0.0.dev64}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: durabletask
3
- Version: 0.0.0.dev63
3
+ Version: 0.0.0.dev64
4
4
  Summary: A Durable Task Client SDK for Python
5
5
  License: MIT License
6
6
 
@@ -6,9 +6,10 @@ import uuid
6
6
  from dataclasses import dataclass
7
7
  from datetime import datetime, timezone
8
8
  from enum import Enum
9
- from typing import Any, List, Optional, Sequence, TypeVar, Union
9
+ from typing import Any, Optional, Sequence, TypeVar, Union
10
10
 
11
11
  import grpc
12
+ from google.protobuf import wrappers_pb2
12
13
 
13
14
  from durabletask.entities import EntityInstanceId
14
15
  from durabletask.entities.entity_metadata import EntityMetadata
@@ -56,39 +57,6 @@ class OrchestrationState:
56
57
  self.failure_details)
57
58
 
58
59
 
59
- @dataclass
60
- class OrchestrationQuery:
61
- created_time_from: Optional[datetime] = None
62
- created_time_to: Optional[datetime] = None
63
- runtime_status: Optional[List[OrchestrationStatus]] = None
64
- # Some backends don't respond well with max_instance_count = None, so we use the integer limit for non-paginated
65
- # results instead.
66
- max_instance_count: Optional[int] = (1 << 31) - 1
67
- fetch_inputs_and_outputs: bool = False
68
-
69
-
70
- @dataclass
71
- class EntityQuery:
72
- instance_id_starts_with: Optional[str] = None
73
- last_modified_from: Optional[datetime] = None
74
- last_modified_to: Optional[datetime] = None
75
- include_state: bool = True
76
- include_transient: bool = False
77
- page_size: Optional[int] = None
78
-
79
-
80
- @dataclass
81
- class PurgeInstancesResult:
82
- deleted_instance_count: int
83
- is_complete: bool
84
-
85
-
86
- @dataclass
87
- class CleanEntityStorageResult:
88
- empty_entities_removed: int
89
- orphaned_locks_released: int
90
-
91
-
92
60
  class OrchestrationFailedError(Exception):
93
61
  def __init__(self, message: str, failure_details: task.FailureDetails):
94
62
  super().__init__(message)
@@ -105,12 +73,6 @@ def new_orchestration_state(instance_id: str, res: pb.GetInstanceResponse) -> Op
105
73
 
106
74
  state = res.orchestrationState
107
75
 
108
- new_state = parse_orchestration_state(state)
109
- new_state.instance_id = instance_id # Override instance_id with the one from the request, to match old behavior
110
- return new_state
111
-
112
-
113
- def parse_orchestration_state(state: pb.OrchestrationState) -> OrchestrationState:
114
76
  failure_details = None
115
77
  if state.failureDetails.errorMessage != '' or state.failureDetails.errorType != '':
116
78
  failure_details = task.FailureDetails(
@@ -119,7 +81,7 @@ def parse_orchestration_state(state: pb.OrchestrationState) -> OrchestrationStat
119
81
  state.failureDetails.stackTrace.value if not helpers.is_empty(state.failureDetails.stackTrace) else None)
120
82
 
121
83
  return OrchestrationState(
122
- state.instanceId,
84
+ instance_id,
123
85
  state.name,
124
86
  OrchestrationStatus(state.orchestrationStatus),
125
87
  state.createdTimestamp.ToDatetime(),
@@ -131,6 +93,7 @@ def parse_orchestration_state(state: pb.OrchestrationState) -> OrchestrationStat
131
93
 
132
94
 
133
95
  class TaskHubGrpcClient:
96
+
134
97
  def __init__(self, *,
135
98
  host_address: Optional[str] = None,
136
99
  metadata: Optional[list[tuple[str, str]]] = None,
@@ -173,7 +136,7 @@ class TaskHubGrpcClient:
173
136
  req = pb.CreateInstanceRequest(
174
137
  name=name,
175
138
  instanceId=instance_id if instance_id else uuid.uuid4().hex,
176
- input=helpers.get_string_value(shared.to_json(input) if input is not None else None),
139
+ input=wrappers_pb2.StringValue(value=shared.to_json(input)) if input is not None else None,
177
140
  scheduledStartTimestamp=helpers.new_timestamp(start_at) if start_at else None,
178
141
  version=helpers.get_string_value(version if version else self.default_version),
179
142
  orchestrationIdReusePolicy=reuse_id_policy,
@@ -189,42 +152,6 @@ class TaskHubGrpcClient:
189
152
  res: pb.GetInstanceResponse = self._stub.GetInstance(req)
190
153
  return new_orchestration_state(req.instanceId, res)
191
154
 
192
- def get_all_orchestration_states(self,
193
- orchestration_query: Optional[OrchestrationQuery] = None
194
- ) -> List[OrchestrationState]:
195
- if orchestration_query is None:
196
- orchestration_query = OrchestrationQuery()
197
- _continuation_token = None
198
-
199
- self._logger.info(f"Querying orchestration instances with query: {orchestration_query}")
200
-
201
- states = []
202
-
203
- while True:
204
- req = pb.QueryInstancesRequest(
205
- query=pb.InstanceQuery(
206
- runtimeStatus=[status.value for status in orchestration_query.runtime_status] if orchestration_query.runtime_status else None,
207
- createdTimeFrom=helpers.new_timestamp(orchestration_query.created_time_from) if orchestration_query.created_time_from else None,
208
- createdTimeTo=helpers.new_timestamp(orchestration_query.created_time_to) if orchestration_query.created_time_to else None,
209
- maxInstanceCount=orchestration_query.max_instance_count,
210
- fetchInputsAndOutputs=orchestration_query.fetch_inputs_and_outputs,
211
- continuationToken=_continuation_token
212
- )
213
- )
214
- resp: pb.QueryInstancesResponse = self._stub.QueryInstances(req)
215
- states += [parse_orchestration_state(res) for res in resp.orchestrationState]
216
- # Check the value for continuationToken - none or "0" indicates that there are no more results.
217
- if resp.continuationToken and resp.continuationToken.value and resp.continuationToken.value != "0":
218
- self._logger.info(f"Received continuation token with value {resp.continuationToken.value}, fetching next list of instances...")
219
- if _continuation_token and _continuation_token.value and _continuation_token.value == resp.continuationToken.value:
220
- self._logger.warning(f"Received the same continuation token value {resp.continuationToken.value} again, stopping to avoid infinite loop.")
221
- break
222
- _continuation_token = resp.continuationToken
223
- else:
224
- break
225
-
226
- return states
227
-
228
155
  def wait_for_orchestration_start(self, instance_id: str, *,
229
156
  fetch_payloads: bool = False,
230
157
  timeout: int = 60) -> Optional[OrchestrationState]:
@@ -272,8 +199,7 @@ class TaskHubGrpcClient:
272
199
  req = pb.RaiseEventRequest(
273
200
  instanceId=instance_id,
274
201
  name=event_name,
275
- input=helpers.get_string_value(shared.to_json(data) if data is not None else None)
276
- )
202
+ input=wrappers_pb2.StringValue(value=shared.to_json(data)) if data else None)
277
203
 
278
204
  self._logger.info(f"Raising event '{event_name}' for instance '{instance_id}'.")
279
205
  self._stub.RaiseEvent(req)
@@ -283,7 +209,7 @@ class TaskHubGrpcClient:
283
209
  recursive: bool = True):
284
210
  req = pb.TerminateRequest(
285
211
  instanceId=instance_id,
286
- output=helpers.get_string_value(shared.to_json(output) if output is not None else None),
212
+ output=wrappers_pb2.StringValue(value=shared.to_json(output)) if output else None,
287
213
  recursive=recursive)
288
214
 
289
215
  self._logger.info(f"Terminating instance '{instance_id}'.")
@@ -299,51 +225,10 @@ class TaskHubGrpcClient:
299
225
  self._logger.info(f"Resuming instance '{instance_id}'.")
300
226
  self._stub.ResumeInstance(req)
301
227
 
302
- def restart_orchestration(self, instance_id: str, *,
303
- restart_with_new_instance_id: bool = False) -> str:
304
- """Restarts an existing orchestration instance.
305
-
306
- Args:
307
- instance_id: The ID of the orchestration instance to restart.
308
- restart_with_new_instance_id: If True, the restarted orchestration will use a new instance ID.
309
- If False (default), the restarted orchestration will reuse the same instance ID.
310
-
311
- Returns:
312
- The instance ID of the restarted orchestration.
313
- """
314
- req = pb.RestartInstanceRequest(
315
- instanceId=instance_id,
316
- restartWithNewInstanceId=restart_with_new_instance_id)
317
-
318
- self._logger.info(f"Restarting instance '{instance_id}'.")
319
- res: pb.RestartInstanceResponse = self._stub.RestartInstance(req)
320
- return res.instanceId
321
-
322
- def purge_orchestration(self, instance_id: str, recursive: bool = True) -> PurgeInstancesResult:
228
+ def purge_orchestration(self, instance_id: str, recursive: bool = True):
323
229
  req = pb.PurgeInstancesRequest(instanceId=instance_id, recursive=recursive)
324
230
  self._logger.info(f"Purging instance '{instance_id}'.")
325
- resp: pb.PurgeInstancesResponse = self._stub.PurgeInstances(req)
326
- return PurgeInstancesResult(resp.deletedInstanceCount, resp.isComplete.value)
327
-
328
- def purge_orchestrations_by(self,
329
- created_time_from: Optional[datetime] = None,
330
- created_time_to: Optional[datetime] = None,
331
- runtime_status: Optional[List[OrchestrationStatus]] = None,
332
- recursive: bool = False) -> PurgeInstancesResult:
333
- self._logger.info("Purging orchestrations by filter: "
334
- f"created_time_from={created_time_from}, "
335
- f"created_time_to={created_time_to}, "
336
- f"runtime_status={[str(status) for status in runtime_status] if runtime_status else None}, "
337
- f"recursive={recursive}")
338
- resp: pb.PurgeInstancesResponse = self._stub.PurgeInstances(pb.PurgeInstancesRequest(
339
- purgeInstanceFilter=pb.PurgeInstanceFilter(
340
- createdTimeFrom=helpers.new_timestamp(created_time_from) if created_time_from else None,
341
- createdTimeTo=helpers.new_timestamp(created_time_to) if created_time_to else None,
342
- runtimeStatus=[status.value for status in runtime_status] if runtime_status else None
343
- ),
344
- recursive=recursive
345
- ))
346
- return PurgeInstancesResult(resp.deletedInstanceCount, resp.isComplete.value)
231
+ self._stub.PurgeInstances(req)
347
232
 
348
233
  def signal_entity(self,
349
234
  entity_instance_id: EntityInstanceId,
@@ -352,7 +237,7 @@ class TaskHubGrpcClient:
352
237
  req = pb.SignalEntityRequest(
353
238
  instanceId=str(entity_instance_id),
354
239
  name=operation_name,
355
- input=helpers.get_string_value(shared.to_json(input) if input is not None else None),
240
+ input=wrappers_pb2.StringValue(value=shared.to_json(input)) if input else None,
356
241
  requestId=str(uuid.uuid4()),
357
242
  scheduledTime=None,
358
243
  parentTraceContext=None,
@@ -371,69 +256,4 @@ class TaskHubGrpcClient:
371
256
  if not res.exists:
372
257
  return None
373
258
 
374
- return EntityMetadata.from_entity_metadata(res.entity, include_state)
375
-
376
- def get_all_entities(self,
377
- entity_query: Optional[EntityQuery] = None) -> List[EntityMetadata]:
378
- if entity_query is None:
379
- entity_query = EntityQuery()
380
- _continuation_token = None
381
-
382
- self._logger.info(f"Retrieving entities by filter: {entity_query}")
383
-
384
- entities = []
385
-
386
- while True:
387
- query_request = pb.QueryEntitiesRequest(
388
- query=pb.EntityQuery(
389
- instanceIdStartsWith=helpers.get_string_value(entity_query.instance_id_starts_with),
390
- lastModifiedFrom=helpers.new_timestamp(entity_query.last_modified_from) if entity_query.last_modified_from else None,
391
- lastModifiedTo=helpers.new_timestamp(entity_query.last_modified_to) if entity_query.last_modified_to else None,
392
- includeState=entity_query.include_state,
393
- includeTransient=entity_query.include_transient,
394
- pageSize=helpers.get_int_value(entity_query.page_size),
395
- continuationToken=_continuation_token
396
- )
397
- )
398
- resp: pb.QueryEntitiesResponse = self._stub.QueryEntities(query_request)
399
- entities += [EntityMetadata.from_entity_metadata(entity, query_request.query.includeState) for entity in resp.entities]
400
- if resp.continuationToken and resp.continuationToken.value and resp.continuationToken.value != "0":
401
- self._logger.info(f"Received continuation token with value {resp.continuationToken.value}, fetching next page of entities...")
402
- if _continuation_token and _continuation_token.value and _continuation_token.value == resp.continuationToken.value:
403
- self._logger.warning(f"Received the same continuation token value {resp.continuationToken.value} again, stopping to avoid infinite loop.")
404
- break
405
- _continuation_token = resp.continuationToken
406
- else:
407
- break
408
- return entities
409
-
410
- def clean_entity_storage(self,
411
- remove_empty_entities: bool = True,
412
- release_orphaned_locks: bool = True
413
- ) -> CleanEntityStorageResult:
414
- self._logger.info("Cleaning entity storage")
415
-
416
- empty_entities_removed = 0
417
- orphaned_locks_released = 0
418
- _continuation_token = None
419
-
420
- while True:
421
- req = pb.CleanEntityStorageRequest(
422
- removeEmptyEntities=remove_empty_entities,
423
- releaseOrphanedLocks=release_orphaned_locks,
424
- continuationToken=_continuation_token
425
- )
426
- resp: pb.CleanEntityStorageResponse = self._stub.CleanEntityStorage(req)
427
- empty_entities_removed += resp.emptyEntitiesRemoved
428
- orphaned_locks_released += resp.orphanedLocksReleased
429
-
430
- if resp.continuationToken and resp.continuationToken.value and resp.continuationToken.value != "0":
431
- self._logger.info(f"Received continuation token with value {resp.continuationToken.value}, cleaning next page...")
432
- if _continuation_token and _continuation_token.value and _continuation_token.value == resp.continuationToken.value:
433
- self._logger.warning(f"Received the same continuation token value {resp.continuationToken.value} again, stopping to avoid infinite loop.")
434
- break
435
- _continuation_token = resp.continuationToken
436
- else:
437
- break
438
-
439
- return CleanEntityStorageResult(empty_entities_removed, orphaned_locks_released)
259
+ return EntityMetadata.from_entity_response(res, include_state)
@@ -8,9 +8,7 @@ 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
12
11
 
13
- __all__ = ["EntityInstanceId", "DurableEntity", "EntityLock", "EntityContext", "EntityMetadata",
14
- "EntityOperationFailedException"]
12
+ __all__ = ["EntityInstanceId", "DurableEntity", "EntityLock", "EntityContext", "EntityMetadata"]
15
13
 
16
14
  PACKAGE_NAME = "durabletask.entities"
@@ -0,0 +1,42 @@
1
+ class EntityInstanceId:
2
+ def __init__(self, entity: str, key: str):
3
+ self.entity = entity
4
+ self.key = key
5
+
6
+ def __str__(self) -> str:
7
+ return f"@{self.entity}@{self.key}"
8
+
9
+ def __eq__(self, other):
10
+ if not isinstance(other, EntityInstanceId):
11
+ return False
12
+ return self.entity == other.entity and self.key == other.key
13
+
14
+ def __lt__(self, other):
15
+ if not isinstance(other, EntityInstanceId):
16
+ return self < other
17
+ return str(self) < str(other)
18
+
19
+ @staticmethod
20
+ def parse(entity_id: str) -> "EntityInstanceId":
21
+ """Parse a string representation of an entity ID into an EntityInstanceId object.
22
+
23
+ Parameters
24
+ ----------
25
+ entity_id : str
26
+ The string representation of the entity ID, in the format '@entity@key'.
27
+
28
+ Returns
29
+ -------
30
+ EntityInstanceId
31
+ The parsed EntityInstanceId object.
32
+
33
+ Raises
34
+ ------
35
+ ValueError
36
+ If the input string is not in the correct format.
37
+ """
38
+ try:
39
+ _, entity, key = entity_id.split("@", 2)
40
+ return EntityInstanceId(entity=entity, key=key)
41
+ except ValueError as ex:
42
+ raise ValueError(f"Invalid entity ID format: {entity_id}", ex)
@@ -44,22 +44,18 @@ 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):
51
47
  try:
52
- entity_id = EntityInstanceId.parse(entity.instanceId)
48
+ entity_id = EntityInstanceId.parse(entity_response.entity.instanceId)
53
49
  except ValueError:
54
50
  raise ValueError("Invalid entity instance ID in entity response.")
55
51
  entity_state = None
56
52
  if includes_state:
57
- entity_state = entity.serializedState.value
53
+ entity_state = entity_response.entity.serializedState.value
58
54
  return EntityMetadata(
59
55
  id=entity_id,
60
- last_modified=entity.lastModifiedTime.ToDatetime(timezone.utc),
61
- backlog_queue_size=entity.backlogQueueSize,
62
- locked_by=entity.lockedBy.value,
56
+ last_modified=entity_response.entity.lastModifiedTime.ToDatetime(timezone.utc),
57
+ backlog_queue_size=entity_response.entity.backlogQueueSize,
58
+ locked_by=entity_response.entity.lockedBy.value,
63
59
  includes_state=includes_state,
64
60
  state=entity_state
65
61
  )
@@ -184,13 +184,6 @@ 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
-
194
187
  def get_string_value_or_empty(val: Optional[str]) -> wrappers_pb2.StringValue:
195
188
  if val is None:
196
189
  return wrappers_pb2.StringValue(value="")