durabletask 0.4.0__tar.gz → 0.5.0__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 (32) hide show
  1. durabletask-0.5.0/PKG-INFO +64 -0
  2. durabletask-0.5.0/README.md +23 -0
  3. {durabletask-0.4.0 → durabletask-0.5.0}/durabletask/client.py +15 -1
  4. durabletask-0.5.0/durabletask/entities/__init__.py +13 -0
  5. durabletask-0.5.0/durabletask/entities/durable_entity.py +93 -0
  6. durabletask-0.5.0/durabletask/entities/entity_context.py +154 -0
  7. durabletask-0.5.0/durabletask/entities/entity_instance_id.py +40 -0
  8. durabletask-0.5.0/durabletask/entities/entity_lock.py +17 -0
  9. durabletask-0.5.0/durabletask/internal/entity_state_shim.py +66 -0
  10. durabletask-0.5.0/durabletask/internal/exceptions.py +11 -0
  11. {durabletask-0.4.0 → durabletask-0.5.0}/durabletask/internal/helpers.py +58 -0
  12. durabletask-0.5.0/durabletask/internal/orchestration_entity_context.py +115 -0
  13. {durabletask-0.4.0 → durabletask-0.5.0}/durabletask/task.py +69 -0
  14. {durabletask-0.4.0 → durabletask-0.5.0}/durabletask/worker.py +489 -32
  15. durabletask-0.5.0/durabletask.egg-info/PKG-INFO +64 -0
  16. {durabletask-0.4.0 → durabletask-0.5.0}/durabletask.egg-info/SOURCES.txt +7 -0
  17. {durabletask-0.4.0 → durabletask-0.5.0}/pyproject.toml +1 -1
  18. durabletask-0.4.0/PKG-INFO +0 -254
  19. durabletask-0.4.0/README.md +0 -213
  20. durabletask-0.4.0/durabletask/internal/exceptions.py +0 -7
  21. durabletask-0.4.0/durabletask.egg-info/PKG-INFO +0 -254
  22. {durabletask-0.4.0 → durabletask-0.5.0}/LICENSE +0 -0
  23. {durabletask-0.4.0 → durabletask-0.5.0}/durabletask/__init__.py +0 -0
  24. {durabletask-0.4.0 → durabletask-0.5.0}/durabletask/internal/grpc_interceptor.py +0 -0
  25. {durabletask-0.4.0 → durabletask-0.5.0}/durabletask/internal/orchestrator_service_pb2.py +0 -0
  26. {durabletask-0.4.0 → durabletask-0.5.0}/durabletask/internal/orchestrator_service_pb2.pyi +0 -0
  27. {durabletask-0.4.0 → durabletask-0.5.0}/durabletask/internal/orchestrator_service_pb2_grpc.py +0 -0
  28. {durabletask-0.4.0 → durabletask-0.5.0}/durabletask/internal/shared.py +0 -0
  29. {durabletask-0.4.0 → durabletask-0.5.0}/durabletask.egg-info/dependency_links.txt +0 -0
  30. {durabletask-0.4.0 → durabletask-0.5.0}/durabletask.egg-info/requires.txt +0 -0
  31. {durabletask-0.4.0 → durabletask-0.5.0}/durabletask.egg-info/top_level.txt +0 -0
  32. {durabletask-0.4.0 → durabletask-0.5.0}/setup.cfg +0 -0
@@ -0,0 +1,64 @@
1
+ Metadata-Version: 2.4
2
+ Name: durabletask
3
+ Version: 0.5.0
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 :: 3 - Alpha
31
+ Classifier: Programming Language :: Python :: 3
32
+ Classifier: License :: OSI Approved :: MIT License
33
+ Requires-Python: >=3.9
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
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
45
+ [![Build Validation](https://github.com/microsoft/durabletask-python/actions/workflows/pr-validation.yml/badge.svg)](https://github.com/microsoft/durabletask-python/actions/workflows/pr-validation.yml)
46
+ [![PyPI version](https://badge.fury.io/py/durabletask.svg)](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
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
4
+ [![Build Validation](https://github.com/microsoft/durabletask-python/actions/workflows/pr-validation.yml/badge.svg)](https://github.com/microsoft/durabletask-python/actions/workflows/pr-validation.yml)
5
+ [![PyPI version](https://badge.fury.io/py/durabletask.svg)](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.
@@ -4,13 +4,14 @@
4
4
  import logging
5
5
  import uuid
6
6
  from dataclasses import dataclass
7
- from datetime import datetime
7
+ from datetime import datetime, timezone
8
8
  from enum import Enum
9
9
  from typing import Any, Optional, Sequence, TypeVar, Union
10
10
 
11
11
  import grpc
12
12
  from google.protobuf import wrappers_pb2
13
13
 
14
+ from durabletask.entities import EntityInstanceId
14
15
  import durabletask.internal.helpers as helpers
15
16
  import durabletask.internal.orchestrator_service_pb2 as pb
16
17
  import durabletask.internal.orchestrator_service_pb2_grpc as stubs
@@ -227,3 +228,16 @@ class TaskHubGrpcClient:
227
228
  req = pb.PurgeInstancesRequest(instanceId=instance_id, recursive=recursive)
228
229
  self._logger.info(f"Purging instance '{instance_id}'.")
229
230
  self._stub.PurgeInstances(req)
231
+
232
+ def signal_entity(self, entity_instance_id: EntityInstanceId, operation_name: str, input: Optional[Any] = None):
233
+ req = pb.SignalEntityRequest(
234
+ instanceId=str(entity_instance_id),
235
+ name=operation_name,
236
+ input=wrappers_pb2.StringValue(value=shared.to_json(input)) if input else None,
237
+ requestId=str(uuid.uuid4()),
238
+ scheduledTime=None,
239
+ parentTraceContext=None,
240
+ requestTime=helpers.new_timestamp(datetime.now(timezone.utc))
241
+ )
242
+ self._logger.info(f"Signaling entity '{entity_instance_id}' operation '{operation_name}'.")
243
+ self._stub.SignalEntity(req, None) # TODO: Cancellation timeout?
@@ -0,0 +1,13 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+
4
+ """Durable Task SDK for Python entities component"""
5
+
6
+ from durabletask.entities.entity_instance_id import EntityInstanceId
7
+ from durabletask.entities.durable_entity import DurableEntity
8
+ from durabletask.entities.entity_lock import EntityLock
9
+ from durabletask.entities.entity_context import EntityContext
10
+
11
+ __all__ = ["EntityInstanceId", "DurableEntity", "EntityLock", "EntityContext"]
12
+
13
+ PACKAGE_NAME = "durabletask.entities"
@@ -0,0 +1,93 @@
1
+ from typing import Any, Optional, Type, TypeVar, Union, overload
2
+
3
+ from durabletask.entities.entity_context import EntityContext
4
+ from durabletask.entities.entity_instance_id import EntityInstanceId
5
+
6
+ TState = TypeVar("TState")
7
+
8
+
9
+ class DurableEntity:
10
+ def _initialize_entity_context(self, context: EntityContext):
11
+ self.entity_context = context
12
+
13
+ @overload
14
+ def get_state(self, intended_type: Type[TState], default: TState) -> TState:
15
+ ...
16
+
17
+ @overload
18
+ def get_state(self, intended_type: Type[TState]) -> Optional[TState]:
19
+ ...
20
+
21
+ @overload
22
+ def get_state(self, intended_type: None = None, default: Any = None) -> Any:
23
+ ...
24
+
25
+ def get_state(self, intended_type: Optional[Type[TState]] = None, default: Optional[TState] = None) -> Union[None, TState, Any]:
26
+ """Get the current state of the entity, optionally converting it to a specified type.
27
+
28
+ Parameters
29
+ ----------
30
+ intended_type : Type[TState] | None, optional
31
+ The type to which the state should be converted. If None, the state is returned as-is.
32
+ default : TState, optional
33
+ The default value to return if the state is not found or cannot be converted.
34
+
35
+ Returns
36
+ -------
37
+ TState | Any
38
+ The current state of the entity, optionally converted to the specified type.
39
+ """
40
+ return self.entity_context.get_state(intended_type, default)
41
+
42
+ def set_state(self, state: Any):
43
+ """Set the state of the entity to a new value.
44
+
45
+ Parameters
46
+ ----------
47
+ new_state : Any
48
+ The new state to set for the entity.
49
+ """
50
+ self.entity_context.set_state(state)
51
+
52
+ def signal_entity(self, entity_instance_id: EntityInstanceId, operation: str, input: Optional[Any] = None) -> None:
53
+ """Signal another entity to perform an operation.
54
+
55
+ Parameters
56
+ ----------
57
+ entity_instance_id : EntityInstanceId
58
+ The ID of the entity instance to signal.
59
+ operation : str
60
+ The operation to perform on the entity.
61
+ input : Any, optional
62
+ The input to provide to the entity for the operation.
63
+ """
64
+ self.entity_context.signal_entity(entity_instance_id, operation, input)
65
+
66
+ def schedule_new_orchestration(self, orchestration_name: str, input: Optional[Any] = None, instance_id: Optional[str] = None) -> str:
67
+ """Schedule a new orchestration instance.
68
+
69
+ Parameters
70
+ ----------
71
+ orchestration_name : str
72
+ The name of the orchestration to schedule.
73
+ input : Any, optional
74
+ The input to provide to the new orchestration.
75
+ instance_id : str, optional
76
+ The instance ID to assign to the new orchestration. If None, a new ID will be generated.
77
+
78
+ Returns
79
+ -------
80
+ str
81
+ The instance ID of the scheduled orchestration.
82
+ """
83
+ return self.entity_context.schedule_new_orchestration(orchestration_name, input, instance_id=instance_id)
84
+
85
+ def delete(self, input: Any = None) -> None:
86
+ """Delete the entity instance.
87
+
88
+ Parameters
89
+ ----------
90
+ input : Any, optional
91
+ Unused: The input for the entity "delete" operation.
92
+ """
93
+ self.set_state(None)
@@ -0,0 +1,154 @@
1
+
2
+ from typing import Any, Optional, Type, TypeVar, Union, overload
3
+ import uuid
4
+ from durabletask.entities.entity_instance_id import EntityInstanceId
5
+ from durabletask.internal import helpers, shared
6
+ from durabletask.internal.entity_state_shim import StateShim
7
+ import durabletask.internal.orchestrator_service_pb2 as pb
8
+
9
+ TState = TypeVar("TState")
10
+
11
+
12
+ class EntityContext:
13
+ def __init__(self, orchestration_id: str, operation: str, state: StateShim, entity_id: EntityInstanceId):
14
+ self._orchestration_id = orchestration_id
15
+ self._operation = operation
16
+ self._state = state
17
+ self._entity_id = entity_id
18
+
19
+ @property
20
+ def orchestration_id(self) -> str:
21
+ """Get the ID of the orchestration instance that scheduled this entity.
22
+
23
+ Returns
24
+ -------
25
+ str
26
+ The ID of the current orchestration instance.
27
+ """
28
+ return self._orchestration_id
29
+
30
+ @property
31
+ def operation(self) -> str:
32
+ """Get the operation associated with this entity invocation.
33
+
34
+ The operation is a string that identifies the specific action being
35
+ performed on the entity. It can be used to distinguish between
36
+ multiple operations that are part of the same entity invocation.
37
+
38
+ Returns
39
+ -------
40
+ str
41
+ The operation associated with this entity invocation.
42
+ """
43
+ return self._operation
44
+
45
+ @overload
46
+ def get_state(self, intended_type: Type[TState], default: TState) -> TState:
47
+ ...
48
+
49
+ @overload
50
+ def get_state(self, intended_type: Type[TState]) -> Optional[TState]:
51
+ ...
52
+
53
+ @overload
54
+ def get_state(self, intended_type: None = None, default: Any = None) -> Any:
55
+ ...
56
+
57
+ def get_state(self, intended_type: Optional[Type[TState]] = None, default: Optional[TState] = None) -> Union[None, TState, Any]:
58
+ """Get the current state of the entity, optionally converting it to a specified type.
59
+
60
+ Parameters
61
+ ----------
62
+ intended_type : Type[TState] | None, optional
63
+ The type to which the state should be converted. If None, the state is returned as-is.
64
+ default : TState, optional
65
+ The default value to return if the state is not found or cannot be converted.
66
+
67
+ Returns
68
+ -------
69
+ TState | Any
70
+ The current state of the entity, optionally converted to the specified type.
71
+ """
72
+ return self._state.get_state(intended_type, default)
73
+
74
+ def set_state(self, new_state: Any):
75
+ """Set the state of the entity to a new value.
76
+
77
+ Parameters
78
+ ----------
79
+ new_state : Any
80
+ The new state to set for the entity.
81
+ """
82
+ self._state.set_state(new_state)
83
+
84
+ def signal_entity(self, entity_instance_id: EntityInstanceId, operation: str, input: Optional[Any] = None) -> None:
85
+ """Signal another entity to perform an operation.
86
+
87
+ Parameters
88
+ ----------
89
+ entity_instance_id : EntityInstanceId
90
+ The ID of the entity instance to signal.
91
+ operation : str
92
+ The operation to perform on the entity.
93
+ input : Any, optional
94
+ The input to provide to the entity for the operation.
95
+ """
96
+ encoded_input = shared.to_json(input) if input is not None else None
97
+ self._state.add_operation_action(
98
+ pb.OperationAction(
99
+ sendSignal=pb.SendSignalAction(
100
+ instanceId=str(entity_instance_id),
101
+ name=operation,
102
+ input=helpers.get_string_value(encoded_input),
103
+ scheduledTime=None,
104
+ requestTime=None,
105
+ parentTraceContext=None,
106
+ )
107
+ )
108
+ )
109
+
110
+ def schedule_new_orchestration(self, orchestration_name: str, input: Optional[Any] = None, instance_id: Optional[str] = None) -> str:
111
+ """Schedule a new orchestration instance.
112
+
113
+ Parameters
114
+ ----------
115
+ orchestration_name : str
116
+ The name of the orchestration to schedule.
117
+ input : Any, optional
118
+ The input to provide to the new orchestration.
119
+ instance_id : str, optional
120
+ The instance ID to assign to the new orchestration. If None, a new ID will be generated.
121
+
122
+ Returns
123
+ -------
124
+ str
125
+ The instance ID of the scheduled orchestration.
126
+ """
127
+ encoded_input = shared.to_json(input) if input is not None else None
128
+ if not instance_id:
129
+ instance_id = uuid.uuid4().hex
130
+ self._state.add_operation_action(
131
+ pb.OperationAction(
132
+ startNewOrchestration=pb.StartNewOrchestrationAction(
133
+ instanceId=instance_id,
134
+ name=orchestration_name,
135
+ input=helpers.get_string_value(encoded_input),
136
+ version=None,
137
+ scheduledTime=None,
138
+ requestTime=None,
139
+ parentTraceContext=None
140
+ )
141
+ )
142
+ )
143
+ return instance_id
144
+
145
+ @property
146
+ def entity_id(self) -> EntityInstanceId:
147
+ """Get the ID of the entity instance.
148
+
149
+ Returns
150
+ -------
151
+ str
152
+ The ID of the current entity instance.
153
+ """
154
+ return self._entity_id
@@ -0,0 +1,40 @@
1
+ from typing import Optional
2
+
3
+
4
+ class EntityInstanceId:
5
+ def __init__(self, entity: str, key: str):
6
+ self.entity = entity
7
+ self.key = key
8
+
9
+ def __str__(self) -> str:
10
+ return f"@{self.entity}@{self.key}"
11
+
12
+ def __eq__(self, other):
13
+ if not isinstance(other, EntityInstanceId):
14
+ return False
15
+ return self.entity == other.entity and self.key == other.key
16
+
17
+ def __lt__(self, other):
18
+ if not isinstance(other, EntityInstanceId):
19
+ return self < other
20
+ return str(self) < str(other)
21
+
22
+ @staticmethod
23
+ def parse(entity_id: str) -> Optional["EntityInstanceId"]:
24
+ """Parse a string representation of an entity ID into an EntityInstanceId object.
25
+
26
+ Parameters
27
+ ----------
28
+ entity_id : str
29
+ The string representation of the entity ID, in the format '@entity@key'.
30
+
31
+ Returns
32
+ -------
33
+ Optional[EntityInstanceId]
34
+ The parsed EntityInstanceId object, or None if the input is None.
35
+ """
36
+ try:
37
+ _, entity, key = entity_id.split("@", 2)
38
+ return EntityInstanceId(entity=entity, key=key)
39
+ except ValueError as ex:
40
+ raise ValueError("Invalid entity ID format", ex)
@@ -0,0 +1,17 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+
4
+ if TYPE_CHECKING:
5
+ from durabletask.task import OrchestrationContext
6
+
7
+
8
+ class EntityLock:
9
+ # Note: This should
10
+ def __init__(self, context: 'OrchestrationContext'):
11
+ self._context = context
12
+
13
+ def __enter__(self):
14
+ return self
15
+
16
+ def __exit__(self, exc_type, exc_val, exc_tb):
17
+ self._context._exit_critical_section()
@@ -0,0 +1,66 @@
1
+ from typing import Any, TypeVar, Union
2
+ from typing import Optional, Type, overload
3
+
4
+ import durabletask.internal.orchestrator_service_pb2 as pb
5
+
6
+ TState = TypeVar("TState")
7
+
8
+
9
+ class StateShim:
10
+ def __init__(self, start_state):
11
+ self._current_state: Any = start_state
12
+ self._checkpoint_state: Any = start_state
13
+ self._operation_actions: list[pb.OperationAction] = []
14
+ self._actions_checkpoint_state: int = 0
15
+
16
+ @overload
17
+ def get_state(self, intended_type: Type[TState], default: TState) -> TState:
18
+ ...
19
+
20
+ @overload
21
+ def get_state(self, intended_type: Type[TState]) -> Optional[TState]:
22
+ ...
23
+
24
+ @overload
25
+ def get_state(self, intended_type: None = None, default: Any = None) -> Any:
26
+ ...
27
+
28
+ def get_state(self, intended_type: Optional[Type[TState]] = None, default: Optional[TState] = None) -> Union[None, TState, Any]:
29
+ if self._current_state is None and default is not None:
30
+ return default
31
+
32
+ if intended_type is None:
33
+ return self._current_state
34
+
35
+ if isinstance(self._current_state, intended_type):
36
+ return self._current_state
37
+
38
+ try:
39
+ return intended_type(self._current_state) # type: ignore[call-arg]
40
+ except Exception as ex:
41
+ raise TypeError(
42
+ f"Could not convert state of type '{type(self._current_state).__name__}' to '{intended_type.__name__}'"
43
+ ) from ex
44
+
45
+ def set_state(self, state):
46
+ self._current_state = state
47
+
48
+ def add_operation_action(self, action: pb.OperationAction):
49
+ self._operation_actions.append(action)
50
+
51
+ def get_operation_actions(self) -> list[pb.OperationAction]:
52
+ return self._operation_actions[:self._actions_checkpoint_state]
53
+
54
+ def commit(self):
55
+ self._checkpoint_state = self._current_state
56
+ self._actions_checkpoint_state = len(self._operation_actions)
57
+
58
+ def rollback(self):
59
+ self._current_state = self._checkpoint_state
60
+ self._operation_actions = self._operation_actions[:self._actions_checkpoint_state]
61
+
62
+ def reset(self):
63
+ self._current_state = None
64
+ self._checkpoint_state = None
65
+ self._operation_actions = []
66
+ self._actions_checkpoint_state = 0
@@ -0,0 +1,11 @@
1
+ import durabletask.internal.orchestrator_service_pb2 as pb
2
+
3
+
4
+ class VersionFailureException(Exception):
5
+ def __init__(self, error_details: pb.TaskFailureDetails) -> None:
6
+ super().__init__()
7
+ self.error_details = error_details
8
+
9
+
10
+ class AbandonOrchestrationError(Exception):
11
+ pass
@@ -7,6 +7,7 @@ from typing import Optional
7
7
 
8
8
  from google.protobuf import timestamp_pb2, wrappers_pb2
9
9
 
10
+ from durabletask.entities import EntityInstanceId
10
11
  import durabletask.internal.orchestrator_service_pb2 as pb
11
12
 
12
13
  # TODO: The new_xxx_event methods are only used by test code and should be moved elsewhere
@@ -159,6 +160,12 @@ def get_string_value(val: Optional[str]) -> Optional[wrappers_pb2.StringValue]:
159
160
  return wrappers_pb2.StringValue(value=val)
160
161
 
161
162
 
163
+ def get_string_value_or_empty(val: Optional[str]) -> wrappers_pb2.StringValue:
164
+ if val is None:
165
+ return wrappers_pb2.StringValue(value="")
166
+ return wrappers_pb2.StringValue(value=val)
167
+
168
+
162
169
  def new_complete_orchestration_action(
163
170
  id: int,
164
171
  status: pb.OrchestrationStatus,
@@ -189,6 +196,57 @@ def new_schedule_task_action(id: int, name: str, encoded_input: Optional[str],
189
196
  ))
190
197
 
191
198
 
199
+ def new_call_entity_action(id: int, parent_instance_id: str, entity_id: EntityInstanceId, operation: str, encoded_input: Optional[str]):
200
+ return pb.OrchestratorAction(id=id, sendEntityMessage=pb.SendEntityMessageAction(entityOperationCalled=pb.EntityOperationCalledEvent(
201
+ requestId=f"{parent_instance_id}:{id}",
202
+ operation=operation,
203
+ scheduledTime=None,
204
+ input=get_string_value(encoded_input),
205
+ parentInstanceId=get_string_value(parent_instance_id),
206
+ parentExecutionId=None,
207
+ targetInstanceId=get_string_value(str(entity_id)),
208
+ )))
209
+
210
+
211
+ def new_signal_entity_action(id: int, entity_id: EntityInstanceId, operation: str, encoded_input: Optional[str]):
212
+ return pb.OrchestratorAction(id=id, sendEntityMessage=pb.SendEntityMessageAction(entityOperationSignaled=pb.EntityOperationSignaledEvent(
213
+ requestId=f"{entity_id}:{id}",
214
+ operation=operation,
215
+ scheduledTime=None,
216
+ input=get_string_value(encoded_input),
217
+ targetInstanceId=get_string_value(str(entity_id)),
218
+ )))
219
+
220
+
221
+ def new_lock_entities_action(id: int, entity_message: pb.SendEntityMessageAction):
222
+ return pb.OrchestratorAction(id=id, sendEntityMessage=entity_message)
223
+
224
+
225
+ def convert_to_entity_batch_request(req: pb.EntityRequest) -> tuple[pb.EntityBatchRequest, list[pb.OperationInfo]]:
226
+ batch_request = pb.EntityBatchRequest(entityState=req.entityState, instanceId=req.instanceId, operations=[])
227
+
228
+ operation_infos: list[pb.OperationInfo] = []
229
+
230
+ for op in req.operationRequests:
231
+ if op.HasField("entityOperationSignaled"):
232
+ batch_request.operations.append(pb.OperationRequest(requestId=op.entityOperationSignaled.requestId,
233
+ operation=op.entityOperationSignaled.operation,
234
+ input=op.entityOperationSignaled.input))
235
+ operation_infos.append(pb.OperationInfo(requestId=op.entityOperationSignaled.requestId,
236
+ responseDestination=None))
237
+ elif op.HasField("entityOperationCalled"):
238
+ batch_request.operations.append(pb.OperationRequest(requestId=op.entityOperationCalled.requestId,
239
+ operation=op.entityOperationCalled.operation,
240
+ input=op.entityOperationCalled.input))
241
+ operation_infos.append(pb.OperationInfo(requestId=op.entityOperationCalled.requestId,
242
+ responseDestination=pb.OrchestrationInstance(
243
+ instanceId=op.entityOperationCalled.parentInstanceId.value,
244
+ executionId=op.entityOperationCalled.parentExecutionId
245
+ )))
246
+
247
+ return batch_request, operation_infos
248
+
249
+
192
250
  def new_timestamp(dt: datetime) -> timestamp_pb2.Timestamp:
193
251
  ts = timestamp_pb2.Timestamp()
194
252
  ts.FromDatetime(dt)