durabletask 0.1.0a1__py3-none-any.whl → 1.0.0__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.
durabletask/__init__.py CHANGED
@@ -3,5 +3,8 @@
3
3
 
4
4
  """Durable Task SDK for Python"""
5
5
 
6
+ from durabletask.worker import ConcurrencyOptions, VersioningOptions
7
+
8
+ __all__ = ["ConcurrencyOptions", "VersioningOptions"]
6
9
 
7
10
  PACKAGE_NAME = "durabletask"
durabletask/client.py CHANGED
@@ -4,18 +4,20 @@
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
- from typing import Any, TypeVar, Union
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
17
18
  import durabletask.internal.shared as shared
18
19
  from durabletask import task
20
+ from durabletask.internal.grpc_interceptor import DefaultClientInterceptorImpl
19
21
 
20
22
  TInput = TypeVar('TInput')
21
23
  TOutput = TypeVar('TOutput')
@@ -42,10 +44,10 @@ class OrchestrationState:
42
44
  runtime_status: OrchestrationStatus
43
45
  created_at: datetime
44
46
  last_updated_at: datetime
45
- serialized_input: Union[str, None]
46
- serialized_output: Union[str, None]
47
- serialized_custom_status: Union[str, None]
48
- failure_details: Union[task.FailureDetails, None]
47
+ serialized_input: Optional[str]
48
+ serialized_output: Optional[str]
49
+ serialized_custom_status: Optional[str]
50
+ failure_details: Optional[task.FailureDetails]
49
51
 
50
52
  def raise_if_failed(self):
51
53
  if self.failure_details is not None:
@@ -64,7 +66,7 @@ class OrchestrationFailedError(Exception):
64
66
  return self._failure_details
65
67
 
66
68
 
67
- def new_orchestration_state(instance_id: str, res: pb.GetInstanceResponse) -> Union[OrchestrationState, None]:
69
+ def new_orchestration_state(instance_id: str, res: pb.GetInstanceResponse) -> Optional[OrchestrationState]:
68
70
  if not res.exists:
69
71
  return None
70
72
 
@@ -92,39 +94,66 @@ def new_orchestration_state(instance_id: str, res: pb.GetInstanceResponse) -> Un
92
94
  class TaskHubGrpcClient:
93
95
 
94
96
  def __init__(self, *,
95
- host_address: Union[str, None] = None,
96
- log_handler=None,
97
- log_formatter: Union[logging.Formatter, None] = None):
98
- channel = shared.get_grpc_channel(host_address)
97
+ host_address: Optional[str] = None,
98
+ metadata: Optional[list[tuple[str, str]]] = None,
99
+ log_handler: Optional[logging.Handler] = None,
100
+ log_formatter: Optional[logging.Formatter] = None,
101
+ secure_channel: bool = False,
102
+ interceptors: Optional[Sequence[shared.ClientInterceptor]] = None,
103
+ default_version: Optional[str] = None):
104
+
105
+ # If the caller provided metadata, we need to create a new interceptor for it and
106
+ # add it to the list of interceptors.
107
+ if interceptors is not None:
108
+ interceptors = list(interceptors)
109
+ if metadata is not None:
110
+ interceptors.append(DefaultClientInterceptorImpl(metadata))
111
+ elif metadata is not None:
112
+ interceptors = [DefaultClientInterceptorImpl(metadata)]
113
+ else:
114
+ interceptors = None
115
+
116
+ channel = shared.get_grpc_channel(
117
+ host_address=host_address,
118
+ secure_channel=secure_channel,
119
+ interceptors=interceptors
120
+ )
99
121
  self._stub = stubs.TaskHubSidecarServiceStub(channel)
100
- self._logger = shared.get_logger(log_handler, log_formatter)
122
+ self._logger = shared.get_logger("client", log_handler, log_formatter)
123
+ self.default_version = default_version
101
124
 
102
125
  def schedule_new_orchestration(self, orchestrator: Union[task.Orchestrator[TInput, TOutput], str], *,
103
- input: Union[TInput, None] = None,
104
- instance_id: Union[str, None] = None,
105
- start_at: Union[datetime, None] = None) -> str:
126
+ input: Optional[TInput] = None,
127
+ instance_id: Optional[str] = None,
128
+ start_at: Optional[datetime] = None,
129
+ reuse_id_policy: Optional[pb.OrchestrationIdReusePolicy] = None,
130
+ tags: Optional[dict[str, str]] = None,
131
+ version: Optional[str] = None) -> str:
106
132
 
107
133
  name = orchestrator if isinstance(orchestrator, str) else task.get_name(orchestrator)
108
134
 
109
135
  req = pb.CreateInstanceRequest(
110
136
  name=name,
111
137
  instanceId=instance_id if instance_id else uuid.uuid4().hex,
112
- input=wrappers_pb2.StringValue(value=shared.to_json(input)) if input else None,
138
+ input=wrappers_pb2.StringValue(value=shared.to_json(input)) if input is not None else None,
113
139
  scheduledStartTimestamp=helpers.new_timestamp(start_at) if start_at else None,
114
- version=wrappers_pb2.StringValue(value=""))
140
+ version=helpers.get_string_value(version if version else self.default_version),
141
+ orchestrationIdReusePolicy=reuse_id_policy,
142
+ tags=tags
143
+ )
115
144
 
116
145
  self._logger.info(f"Starting new '{name}' instance with ID = '{req.instanceId}'.")
117
146
  res: pb.CreateInstanceResponse = self._stub.StartInstance(req)
118
147
  return res.instanceId
119
148
 
120
- def get_orchestration_state(self, instance_id: str, *, fetch_payloads: bool = True) -> Union[OrchestrationState, None]:
149
+ def get_orchestration_state(self, instance_id: str, *, fetch_payloads: bool = True) -> Optional[OrchestrationState]:
121
150
  req = pb.GetInstanceRequest(instanceId=instance_id, getInputsAndOutputs=fetch_payloads)
122
151
  res: pb.GetInstanceResponse = self._stub.GetInstance(req)
123
152
  return new_orchestration_state(req.instanceId, res)
124
153
 
125
154
  def wait_for_orchestration_start(self, instance_id: str, *,
126
155
  fetch_payloads: bool = False,
127
- timeout: int = 60) -> Union[OrchestrationState, None]:
156
+ timeout: int = 60) -> Optional[OrchestrationState]:
128
157
  req = pb.GetInstanceRequest(instanceId=instance_id, getInputsAndOutputs=fetch_payloads)
129
158
  try:
130
159
  self._logger.info(f"Waiting up to {timeout}s for instance '{instance_id}' to start.")
@@ -139,12 +168,24 @@ class TaskHubGrpcClient:
139
168
 
140
169
  def wait_for_orchestration_completion(self, instance_id: str, *,
141
170
  fetch_payloads: bool = True,
142
- timeout: int = 60) -> Union[OrchestrationState, None]:
171
+ timeout: int = 60) -> Optional[OrchestrationState]:
143
172
  req = pb.GetInstanceRequest(instanceId=instance_id, getInputsAndOutputs=fetch_payloads)
144
173
  try:
145
174
  self._logger.info(f"Waiting {timeout}s for instance '{instance_id}' to complete.")
146
175
  res: pb.GetInstanceResponse = self._stub.WaitForInstanceCompletion(req, timeout=timeout)
147
- return new_orchestration_state(req.instanceId, res)
176
+ state = new_orchestration_state(req.instanceId, res)
177
+ if not state:
178
+ return None
179
+
180
+ if state.runtime_status == OrchestrationStatus.FAILED and state.failure_details is not None:
181
+ details = state.failure_details
182
+ self._logger.info(f"Instance '{instance_id}' failed: [{details.error_type}] {details.message}")
183
+ elif state.runtime_status == OrchestrationStatus.TERMINATED:
184
+ self._logger.info(f"Instance '{instance_id}' was terminated.")
185
+ elif state.runtime_status == OrchestrationStatus.COMPLETED:
186
+ self._logger.info(f"Instance '{instance_id}' completed.")
187
+
188
+ return state
148
189
  except grpc.RpcError as rpc_error:
149
190
  if rpc_error.code() == grpc.StatusCode.DEADLINE_EXCEEDED: # type: ignore
150
191
  # Replace gRPC error with the built-in TimeoutError
@@ -153,7 +194,7 @@ class TaskHubGrpcClient:
153
194
  raise
154
195
 
155
196
  def raise_orchestration_event(self, instance_id: str, event_name: str, *,
156
- data: Union[Any, None] = None):
197
+ data: Optional[Any] = None):
157
198
  req = pb.RaiseEventRequest(
158
199
  instanceId=instance_id,
159
200
  name=event_name,
@@ -163,10 +204,12 @@ class TaskHubGrpcClient:
163
204
  self._stub.RaiseEvent(req)
164
205
 
165
206
  def terminate_orchestration(self, instance_id: str, *,
166
- output: Union[Any, None] = None):
207
+ output: Optional[Any] = None,
208
+ recursive: bool = True):
167
209
  req = pb.TerminateRequest(
168
210
  instanceId=instance_id,
169
- output=wrappers_pb2.StringValue(value=shared.to_json(output)) if output else None)
211
+ output=wrappers_pb2.StringValue(value=shared.to_json(output)) if output else None,
212
+ recursive=recursive)
170
213
 
171
214
  self._logger.info(f"Terminating instance '{instance_id}'.")
172
215
  self._stub.TerminateInstance(req)
@@ -180,3 +223,21 @@ class TaskHubGrpcClient:
180
223
  req = pb.ResumeRequest(instanceId=instance_id)
181
224
  self._logger.info(f"Resuming instance '{instance_id}'.")
182
225
  self._stub.ResumeInstance(req)
226
+
227
+ def purge_orchestration(self, instance_id: str, recursive: bool = True):
228
+ req = pb.PurgeInstancesRequest(instanceId=instance_id, recursive=recursive)
229
+ self._logger.info(f"Purging instance '{instance_id}'.")
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
@@ -0,0 +1,65 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+
4
+ from collections import namedtuple
5
+
6
+ import grpc
7
+
8
+
9
+ class _ClientCallDetails(
10
+ namedtuple(
11
+ '_ClientCallDetails',
12
+ ['method', 'timeout', 'metadata', 'credentials', 'wait_for_ready', 'compression']),
13
+ grpc.ClientCallDetails):
14
+ """This is an implementation of the ClientCallDetails interface needed for interceptors.
15
+ This class takes six named values and inherits the ClientCallDetails from grpc package.
16
+ This class encloses the values that describe a RPC to be invoked.
17
+ """
18
+ pass
19
+
20
+
21
+ class DefaultClientInterceptorImpl (
22
+ grpc.UnaryUnaryClientInterceptor, grpc.UnaryStreamClientInterceptor,
23
+ grpc.StreamUnaryClientInterceptor, grpc.StreamStreamClientInterceptor):
24
+ """The class implements a UnaryUnaryClientInterceptor, UnaryStreamClientInterceptor,
25
+ StreamUnaryClientInterceptor and StreamStreamClientInterceptor from grpc to add an
26
+ interceptor to add additional headers to all calls as needed."""
27
+
28
+ def __init__(self, metadata: list[tuple[str, str]]):
29
+ super().__init__()
30
+ self._metadata = metadata
31
+
32
+ def _intercept_call(
33
+ self, client_call_details: _ClientCallDetails) -> grpc.ClientCallDetails:
34
+ """Internal intercept_call implementation which adds metadata to grpc metadata in the RPC
35
+ call details."""
36
+ if self._metadata is None:
37
+ return client_call_details
38
+
39
+ if client_call_details.metadata is not None:
40
+ metadata = list(client_call_details.metadata)
41
+ else:
42
+ metadata = []
43
+
44
+ metadata.extend(self._metadata)
45
+ client_call_details = _ClientCallDetails(
46
+ client_call_details.method, client_call_details.timeout, metadata,
47
+ client_call_details.credentials, client_call_details.wait_for_ready, client_call_details.compression)
48
+
49
+ return client_call_details
50
+
51
+ def intercept_unary_unary(self, continuation, client_call_details, request):
52
+ new_client_call_details = self._intercept_call(client_call_details)
53
+ return continuation(new_client_call_details, request)
54
+
55
+ def intercept_unary_stream(self, continuation, client_call_details, request):
56
+ new_client_call_details = self._intercept_call(client_call_details)
57
+ return continuation(new_client_call_details, request)
58
+
59
+ def intercept_stream_unary(self, continuation, client_call_details, request):
60
+ new_client_call_details = self._intercept_call(client_call_details)
61
+ return continuation(new_client_call_details, request)
62
+
63
+ def intercept_stream_stream(self, continuation, client_call_details, request):
64
+ new_client_call_details = self._intercept_call(client_call_details)
65
+ return continuation(new_client_call_details, request)