durabletask 1.5.0__tar.gz → 1.7.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.
- {durabletask-1.5.0 → durabletask-1.7.0}/PKG-INFO +3 -1
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/client.py +143 -30
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/entities/durable_entity.py +12 -2
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/entities/entity_context.py +26 -7
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/entities/entity_instance_id.py +3 -0
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/entities/entity_lock.py +3 -0
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/entities/entity_metadata.py +50 -7
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/entities/entity_operation_failed_exception.py +3 -0
- durabletask-1.7.0/durabletask/extensions/history_export/__init__.py +80 -0
- durabletask-1.7.0/durabletask/extensions/history_export/_constants.py +34 -0
- durabletask-1.7.0/durabletask/extensions/history_export/_internal.py +38 -0
- durabletask-1.7.0/durabletask/extensions/history_export/_logging.py +19 -0
- durabletask-1.7.0/durabletask/extensions/history_export/activities.py +341 -0
- durabletask-1.7.0/durabletask/extensions/history_export/azure_blob.py +195 -0
- durabletask-1.7.0/durabletask/extensions/history_export/client.py +474 -0
- durabletask-1.7.0/durabletask/extensions/history_export/entity.py +341 -0
- durabletask-1.7.0/durabletask/extensions/history_export/exceptions.py +55 -0
- durabletask-1.7.0/durabletask/extensions/history_export/models.py +594 -0
- durabletask-1.7.0/durabletask/extensions/history_export/orchestrator.py +412 -0
- durabletask-1.7.0/durabletask/extensions/history_export/serialization.py +182 -0
- durabletask-1.7.0/durabletask/extensions/history_export/transitions.py +80 -0
- durabletask-1.7.0/durabletask/extensions/history_export/writer.py +122 -0
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/internal/client_helpers.py +24 -9
- durabletask-1.7.0/durabletask/internal/entity_state_shim.py +128 -0
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/internal/exceptions.py +3 -0
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/internal/helpers.py +20 -3
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/internal/json_encode_output_exception.py +3 -0
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/internal/orchestration_entity_context.py +3 -0
- durabletask-1.7.0/durabletask/internal/orchestrator_service_pb2.py +330 -0
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/internal/orchestrator_service_pb2.pyi +19 -6
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/internal/proto_task_hub_sidecar_service_stub.py +3 -0
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/internal/shared.py +45 -55
- durabletask-1.7.0/durabletask/internal/type_discovery.py +150 -0
- durabletask-1.7.0/durabletask/scheduled/__init__.py +36 -0
- durabletask-1.7.0/durabletask/scheduled/client.py +154 -0
- durabletask-1.7.0/durabletask/scheduled/exceptions.py +36 -0
- durabletask-1.7.0/durabletask/scheduled/models.py +326 -0
- durabletask-1.7.0/durabletask/scheduled/orchestrator.py +37 -0
- durabletask-1.7.0/durabletask/scheduled/schedule_entity.py +236 -0
- durabletask-1.7.0/durabletask/scheduled/schedule_status.py +17 -0
- durabletask-1.7.0/durabletask/scheduled/transitions.py +47 -0
- durabletask-1.7.0/durabletask/serialization.py +567 -0
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/task.py +110 -11
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/testing/in_memory_backend.py +68 -8
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/worker.py +288 -100
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask.egg-info/PKG-INFO +3 -1
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask.egg-info/SOURCES.txt +24 -0
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask.egg-info/requires.txt +3 -0
- {durabletask-1.5.0 → durabletask-1.7.0}/pyproject.toml +4 -1
- durabletask-1.5.0/durabletask/internal/entity_state_shim.py +0 -65
- durabletask-1.5.0/durabletask/internal/orchestrator_service_pb2.py +0 -322
- {durabletask-1.5.0 → durabletask-1.7.0}/LICENSE +0 -0
- {durabletask-1.5.0 → durabletask-1.7.0}/README.md +0 -0
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/__init__.py +0 -0
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/entities/__init__.py +0 -0
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/extensions/__init__.py +0 -0
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/extensions/azure_blob_payloads/__init__.py +0 -0
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/extensions/azure_blob_payloads/blob_payload_store.py +0 -0
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/extensions/azure_blob_payloads/options.py +0 -0
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/grpc_options.py +0 -0
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/history.py +0 -0
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/internal/grpc_interceptor.py +0 -0
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/internal/grpc_resiliency.py +0 -0
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/internal/history_helpers.py +0 -0
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/internal/orchestrator_service_pb2_grpc.py +0 -0
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/internal/tracing.py +0 -0
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/payload/__init__.py +0 -0
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/payload/helpers.py +0 -0
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/payload/store.py +0 -0
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/py.typed +0 -0
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask/testing/__init__.py +0 -0
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask.egg-info/dependency_links.txt +0 -0
- {durabletask-1.5.0 → durabletask-1.7.0}/durabletask.egg-info/top_level.txt +0 -0
- {durabletask-1.5.0 → durabletask-1.7.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: durabletask
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.0
|
|
4
4
|
Summary: A Durable Task Client SDK for Python
|
|
5
5
|
License: MIT License
|
|
6
6
|
|
|
@@ -42,6 +42,8 @@ Requires-Dist: opentelemetry-api>=1.0.0; extra == "opentelemetry"
|
|
|
42
42
|
Requires-Dist: opentelemetry-sdk>=1.0.0; extra == "opentelemetry"
|
|
43
43
|
Provides-Extra: azure-blob-payloads
|
|
44
44
|
Requires-Dist: azure-storage-blob[aio]>=12.0.0; extra == "azure-blob-payloads"
|
|
45
|
+
Provides-Extra: history-export-azure
|
|
46
|
+
Requires-Dist: azure-storage-blob[aio]>=12.0.0; extra == "history-export-azure"
|
|
45
47
|
Dynamic: license-file
|
|
46
48
|
|
|
47
49
|
# Durable Task SDK for Python
|
|
@@ -7,10 +7,10 @@ import threading
|
|
|
7
7
|
import time
|
|
8
8
|
import uuid
|
|
9
9
|
from collections.abc import AsyncIterable, Iterable, Sequence
|
|
10
|
-
from dataclasses import dataclass
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
11
|
from datetime import datetime
|
|
12
12
|
from enum import Enum
|
|
13
|
-
from typing import Any, Generic, Protocol, TypeVar, cast
|
|
13
|
+
from typing import Any, Generic, Protocol, TypeVar, cast, overload
|
|
14
14
|
|
|
15
15
|
import grpc
|
|
16
16
|
import grpc.aio
|
|
@@ -50,10 +50,12 @@ from durabletask.internal.client_helpers import (
|
|
|
50
50
|
)
|
|
51
51
|
from durabletask.payload import helpers as payload_helpers
|
|
52
52
|
from durabletask.payload.store import PayloadStore
|
|
53
|
+
from durabletask.serialization import DEFAULT_DATA_CONVERTER, DataConverter, JsonDataConverter
|
|
53
54
|
|
|
54
55
|
TInput = TypeVar('TInput')
|
|
55
56
|
TOutput = TypeVar('TOutput')
|
|
56
57
|
TItem = TypeVar('TItem')
|
|
58
|
+
T = TypeVar('T')
|
|
57
59
|
|
|
58
60
|
|
|
59
61
|
class OrchestrationStatus(Enum):
|
|
@@ -81,6 +83,102 @@ class OrchestrationState:
|
|
|
81
83
|
serialized_output: str | None
|
|
82
84
|
serialized_custom_status: str | None
|
|
83
85
|
failure_details: task.FailureDetails | None
|
|
86
|
+
# Converter used by the typed accessors below. Defaults to the SDK's JSON
|
|
87
|
+
# converter; the client populates it with its own converter so custom
|
|
88
|
+
# serialization applies on the read side too. Excluded from equality/repr so
|
|
89
|
+
# two states with equal payloads remain equal regardless of converter.
|
|
90
|
+
_data_converter: DataConverter = field(
|
|
91
|
+
default=DEFAULT_DATA_CONVERTER, compare=False, repr=False)
|
|
92
|
+
|
|
93
|
+
@overload
|
|
94
|
+
def get_input(self, expected_type: type[T]) -> T | None:
|
|
95
|
+
...
|
|
96
|
+
|
|
97
|
+
@overload
|
|
98
|
+
def get_input(self, expected_type: None = ...) -> Any:
|
|
99
|
+
...
|
|
100
|
+
|
|
101
|
+
def get_input(self, expected_type: type | None = None) -> Any:
|
|
102
|
+
"""Deserialize the orchestration's input.
|
|
103
|
+
|
|
104
|
+
Parameters
|
|
105
|
+
----------
|
|
106
|
+
expected_type : type | None
|
|
107
|
+
Optional type used to reconstruct the input. When provided, the
|
|
108
|
+
payload is coerced to this type (dataclasses are constructed from
|
|
109
|
+
their dict payloads, types exposing a ``from_json()`` classmethod
|
|
110
|
+
are reconstructed via that hook) and the return value is typed as
|
|
111
|
+
``expected_type | None``. When omitted, the raw deserialized JSON is
|
|
112
|
+
returned.
|
|
113
|
+
|
|
114
|
+
Returns
|
|
115
|
+
-------
|
|
116
|
+
Any
|
|
117
|
+
The deserialized input, or None if there is no input.
|
|
118
|
+
"""
|
|
119
|
+
if self.serialized_input is None:
|
|
120
|
+
return None
|
|
121
|
+
return self._data_converter.deserialize(self.serialized_input, expected_type)
|
|
122
|
+
|
|
123
|
+
@overload
|
|
124
|
+
def get_output(self, expected_type: type[T]) -> T | None:
|
|
125
|
+
...
|
|
126
|
+
|
|
127
|
+
@overload
|
|
128
|
+
def get_output(self, expected_type: None = ...) -> Any:
|
|
129
|
+
...
|
|
130
|
+
|
|
131
|
+
def get_output(self, expected_type: type | None = None) -> Any:
|
|
132
|
+
"""Deserialize the orchestration's output.
|
|
133
|
+
|
|
134
|
+
Parameters
|
|
135
|
+
----------
|
|
136
|
+
expected_type : type | None
|
|
137
|
+
Optional type used to reconstruct the output. When provided, the
|
|
138
|
+
payload is coerced to this type (dataclasses are constructed from
|
|
139
|
+
their dict payloads, types exposing a ``from_json()`` classmethod
|
|
140
|
+
are reconstructed via that hook) and the return value is typed as
|
|
141
|
+
``expected_type | None``. When omitted, the raw deserialized JSON is
|
|
142
|
+
returned.
|
|
143
|
+
|
|
144
|
+
Returns
|
|
145
|
+
-------
|
|
146
|
+
Any
|
|
147
|
+
The deserialized output, or None if there is no output.
|
|
148
|
+
"""
|
|
149
|
+
if self.serialized_output is None:
|
|
150
|
+
return None
|
|
151
|
+
return self._data_converter.deserialize(self.serialized_output, expected_type)
|
|
152
|
+
|
|
153
|
+
@overload
|
|
154
|
+
def get_custom_status(self, expected_type: type[T]) -> T | None:
|
|
155
|
+
...
|
|
156
|
+
|
|
157
|
+
@overload
|
|
158
|
+
def get_custom_status(self, expected_type: None = ...) -> Any:
|
|
159
|
+
...
|
|
160
|
+
|
|
161
|
+
def get_custom_status(self, expected_type: type | None = None) -> Any:
|
|
162
|
+
"""Deserialize the orchestration's custom status.
|
|
163
|
+
|
|
164
|
+
Parameters
|
|
165
|
+
----------
|
|
166
|
+
expected_type : type | None
|
|
167
|
+
Optional type used to reconstruct the custom status. When provided,
|
|
168
|
+
the payload is coerced to this type (dataclasses are constructed
|
|
169
|
+
from their dict payloads, types exposing a ``from_json()``
|
|
170
|
+
classmethod are reconstructed via that hook) and the return value is
|
|
171
|
+
typed as ``expected_type | None``. When omitted, the raw
|
|
172
|
+
deserialized JSON is returned.
|
|
173
|
+
|
|
174
|
+
Returns
|
|
175
|
+
-------
|
|
176
|
+
Any
|
|
177
|
+
The deserialized custom status, or None if there is no custom status.
|
|
178
|
+
"""
|
|
179
|
+
if self.serialized_custom_status is None:
|
|
180
|
+
return None
|
|
181
|
+
return self._data_converter.deserialize(self.serialized_custom_status, expected_type)
|
|
84
182
|
|
|
85
183
|
def raise_if_failed(self):
|
|
86
184
|
if self.failure_details is not None:
|
|
@@ -138,18 +236,22 @@ class OrchestrationFailedError(Exception):
|
|
|
138
236
|
return self._failure_details
|
|
139
237
|
|
|
140
238
|
|
|
141
|
-
def new_orchestration_state(
|
|
239
|
+
def new_orchestration_state(
|
|
240
|
+
instance_id: str, res: pb.GetInstanceResponse,
|
|
241
|
+
data_converter: DataConverter | None = None) -> OrchestrationState | None:
|
|
142
242
|
if not res.exists:
|
|
143
243
|
return None
|
|
144
244
|
|
|
145
245
|
state = res.orchestrationState
|
|
146
246
|
|
|
147
|
-
new_state = parse_orchestration_state(state)
|
|
247
|
+
new_state = parse_orchestration_state(state, data_converter)
|
|
148
248
|
new_state.instance_id = instance_id # Override instance_id with the one from the request, to match old behavior
|
|
149
249
|
return new_state
|
|
150
250
|
|
|
151
251
|
|
|
152
|
-
def parse_orchestration_state(
|
|
252
|
+
def parse_orchestration_state(
|
|
253
|
+
state: pb.OrchestrationState,
|
|
254
|
+
data_converter: DataConverter | None = None) -> OrchestrationState:
|
|
153
255
|
failure_details = None
|
|
154
256
|
if state.failureDetails.errorMessage != '' or state.failureDetails.errorType != '':
|
|
155
257
|
failure_details = task.FailureDetails(
|
|
@@ -166,7 +268,8 @@ def parse_orchestration_state(state: pb.OrchestrationState) -> OrchestrationStat
|
|
|
166
268
|
state.input.value if not helpers.is_empty(state.input) else None,
|
|
167
269
|
state.output.value if not helpers.is_empty(state.output) else None,
|
|
168
270
|
state.customStatus.value if not helpers.is_empty(state.customStatus) else None,
|
|
169
|
-
failure_details
|
|
271
|
+
failure_details,
|
|
272
|
+
data_converter if data_converter is not None else DEFAULT_DATA_CONVERTER)
|
|
170
273
|
|
|
171
274
|
|
|
172
275
|
# Grace period before a retired SDK-owned channel is force-closed. Long enough
|
|
@@ -309,9 +412,11 @@ class TaskHubGrpcClient:
|
|
|
309
412
|
channel_options: GrpcChannelOptions | None = None,
|
|
310
413
|
resiliency_options: GrpcClientResiliencyOptions | None = None,
|
|
311
414
|
default_version: str | None = None,
|
|
312
|
-
payload_store: PayloadStore | None = None
|
|
415
|
+
payload_store: PayloadStore | None = None,
|
|
416
|
+
data_converter: DataConverter | None = None):
|
|
313
417
|
|
|
314
418
|
self._owns_channel = channel is None
|
|
419
|
+
self._data_converter = data_converter if data_converter is not None else JsonDataConverter()
|
|
315
420
|
self._host_address = (
|
|
316
421
|
host_address if host_address else shared.get_default_host_address()
|
|
317
422
|
)
|
|
@@ -514,7 +619,8 @@ class TaskHubGrpcClient:
|
|
|
514
619
|
req = build_schedule_new_orchestration_req(
|
|
515
620
|
orchestrator, input=input, instance_id=instance_id, start_at=start_at,
|
|
516
621
|
reuse_id_policy=reuse_id_policy, tags=tags,
|
|
517
|
-
version=version if version else self.default_version
|
|
622
|
+
version=version if version else self.default_version,
|
|
623
|
+
data_converter=self._data_converter)
|
|
518
624
|
|
|
519
625
|
# Inject the active PRODUCER span context into the request so the sidecar
|
|
520
626
|
# stores it in the executionStarted event and the worker can parent all
|
|
@@ -538,7 +644,7 @@ class TaskHubGrpcClient:
|
|
|
538
644
|
# De-externalize any large-payload tokens in the response
|
|
539
645
|
if self._payload_store is not None and res.exists:
|
|
540
646
|
payload_helpers.deexternalize_payloads(res, self._payload_store)
|
|
541
|
-
return new_orchestration_state(req.instanceId, res)
|
|
647
|
+
return new_orchestration_state(req.instanceId, res, self._data_converter)
|
|
542
648
|
|
|
543
649
|
def get_orchestration_history(self,
|
|
544
650
|
instance_id: str, *,
|
|
@@ -594,7 +700,7 @@ class TaskHubGrpcClient:
|
|
|
594
700
|
resp: pb.QueryInstancesResponse = self._stub.QueryInstances(req)
|
|
595
701
|
if self._payload_store is not None:
|
|
596
702
|
payload_helpers.deexternalize_payloads(resp, self._payload_store)
|
|
597
|
-
states += [parse_orchestration_state(res) for res in resp.orchestrationState]
|
|
703
|
+
states += [parse_orchestration_state(res, self._data_converter) for res in resp.orchestrationState]
|
|
598
704
|
if check_continuation_token(resp.continuationToken, _continuation_token, self._logger):
|
|
599
705
|
_continuation_token = resp.continuationToken
|
|
600
706
|
else:
|
|
@@ -614,7 +720,7 @@ class TaskHubGrpcClient:
|
|
|
614
720
|
)
|
|
615
721
|
if self._payload_store is not None and res.exists:
|
|
616
722
|
payload_helpers.deexternalize_payloads(res, self._payload_store)
|
|
617
|
-
return new_orchestration_state(req.instanceId, res)
|
|
723
|
+
return new_orchestration_state(req.instanceId, res, self._data_converter)
|
|
618
724
|
except grpc.RpcError as rpc_error:
|
|
619
725
|
if rpc_error.code() == grpc.StatusCode.DEADLINE_EXCEEDED: # type: ignore
|
|
620
726
|
# Replace gRPC error with the built-in TimeoutError
|
|
@@ -634,7 +740,7 @@ class TaskHubGrpcClient:
|
|
|
634
740
|
)
|
|
635
741
|
if self._payload_store is not None and res.exists:
|
|
636
742
|
payload_helpers.deexternalize_payloads(res, self._payload_store)
|
|
637
|
-
state = new_orchestration_state(req.instanceId, res)
|
|
743
|
+
state = new_orchestration_state(req.instanceId, res, self._data_converter)
|
|
638
744
|
log_completion_state(self._logger, instance_id, state)
|
|
639
745
|
return state
|
|
640
746
|
except grpc.RpcError as rpc_error:
|
|
@@ -646,7 +752,7 @@ class TaskHubGrpcClient:
|
|
|
646
752
|
def raise_orchestration_event(self, instance_id: str, event_name: str, *,
|
|
647
753
|
data: Any | None = None) -> None:
|
|
648
754
|
with tracing.start_raise_event_span(event_name, instance_id):
|
|
649
|
-
req = build_raise_event_req(instance_id, event_name, data)
|
|
755
|
+
req = build_raise_event_req(instance_id, event_name, data, self._data_converter)
|
|
650
756
|
self._logger.info(f"Raising event '{event_name}' for instance '{instance_id}'.")
|
|
651
757
|
if self._payload_store is not None:
|
|
652
758
|
payload_helpers.externalize_payloads(
|
|
@@ -657,7 +763,7 @@ class TaskHubGrpcClient:
|
|
|
657
763
|
def terminate_orchestration(self, instance_id: str, *,
|
|
658
764
|
output: Any | None = None,
|
|
659
765
|
recursive: bool = True) -> None:
|
|
660
|
-
req = build_terminate_req(instance_id, output, recursive)
|
|
766
|
+
req = build_terminate_req(instance_id, output, recursive, self._data_converter)
|
|
661
767
|
|
|
662
768
|
self._logger.info(f"Terminating instance '{instance_id}'.")
|
|
663
769
|
if self._payload_store is not None:
|
|
@@ -719,8 +825,10 @@ class TaskHubGrpcClient:
|
|
|
719
825
|
def signal_entity(self,
|
|
720
826
|
entity_instance_id: EntityInstanceId,
|
|
721
827
|
operation_name: str,
|
|
722
|
-
input: Any | None = None
|
|
723
|
-
|
|
828
|
+
input: Any | None = None,
|
|
829
|
+
signal_time: datetime | None = None) -> None:
|
|
830
|
+
req = build_signal_entity_req(
|
|
831
|
+
entity_instance_id, operation_name, input, signal_time, self._data_converter)
|
|
724
832
|
self._logger.info(f"Signaling entity '{entity_instance_id}' operation '{operation_name}'.")
|
|
725
833
|
if self._payload_store is not None:
|
|
726
834
|
payload_helpers.externalize_payloads(
|
|
@@ -739,7 +847,7 @@ class TaskHubGrpcClient:
|
|
|
739
847
|
return None
|
|
740
848
|
if self._payload_store is not None:
|
|
741
849
|
payload_helpers.deexternalize_payloads(res, self._payload_store)
|
|
742
|
-
return EntityMetadata.from_entity_metadata(res.entity, include_state)
|
|
850
|
+
return EntityMetadata.from_entity_metadata(res.entity, include_state, self._data_converter)
|
|
743
851
|
|
|
744
852
|
def get_all_entities(self,
|
|
745
853
|
entity_query: EntityQuery | None = None) -> list[EntityMetadata]:
|
|
@@ -756,7 +864,7 @@ class TaskHubGrpcClient:
|
|
|
756
864
|
resp: pb.QueryEntitiesResponse = self._stub.QueryEntities(query_request)
|
|
757
865
|
if self._payload_store is not None:
|
|
758
866
|
payload_helpers.deexternalize_payloads(resp, self._payload_store)
|
|
759
|
-
entities += [EntityMetadata.from_entity_metadata(entity, query_request.query.includeState) for entity in resp.entities]
|
|
867
|
+
entities += [EntityMetadata.from_entity_metadata(entity, query_request.query.includeState, self._data_converter) for entity in resp.entities]
|
|
760
868
|
if check_continuation_token(resp.continuationToken, _continuation_token, self._logger):
|
|
761
869
|
_continuation_token = resp.continuationToken
|
|
762
870
|
else:
|
|
@@ -805,9 +913,11 @@ class AsyncTaskHubGrpcClient:
|
|
|
805
913
|
channel_options: GrpcChannelOptions | None = None,
|
|
806
914
|
resiliency_options: GrpcClientResiliencyOptions | None = None,
|
|
807
915
|
default_version: str | None = None,
|
|
808
|
-
payload_store: PayloadStore | None = None
|
|
916
|
+
payload_store: PayloadStore | None = None,
|
|
917
|
+
data_converter: DataConverter | None = None):
|
|
809
918
|
|
|
810
919
|
self._owns_channel = channel is None
|
|
920
|
+
self._data_converter = data_converter if data_converter is not None else JsonDataConverter()
|
|
811
921
|
self._host_address = (
|
|
812
922
|
host_address if host_address else shared.get_default_host_address()
|
|
813
923
|
)
|
|
@@ -998,7 +1108,8 @@ class AsyncTaskHubGrpcClient:
|
|
|
998
1108
|
req = build_schedule_new_orchestration_req(
|
|
999
1109
|
orchestrator, input=input, instance_id=instance_id, start_at=start_at,
|
|
1000
1110
|
reuse_id_policy=reuse_id_policy, tags=tags,
|
|
1001
|
-
version=version if version else self.default_version
|
|
1111
|
+
version=version if version else self.default_version,
|
|
1112
|
+
data_converter=self._data_converter)
|
|
1002
1113
|
|
|
1003
1114
|
parent_trace_ctx = tracing.get_current_trace_context()
|
|
1004
1115
|
if parent_trace_ctx is not None:
|
|
@@ -1019,7 +1130,7 @@ class AsyncTaskHubGrpcClient:
|
|
|
1019
1130
|
res: pb.GetInstanceResponse = await self._stub.GetInstance(req)
|
|
1020
1131
|
if self._payload_store is not None and res.exists:
|
|
1021
1132
|
await payload_helpers.deexternalize_payloads_async(res, self._payload_store)
|
|
1022
|
-
return new_orchestration_state(req.instanceId, res)
|
|
1133
|
+
return new_orchestration_state(req.instanceId, res, self._data_converter)
|
|
1023
1134
|
|
|
1024
1135
|
async def get_orchestration_history(self,
|
|
1025
1136
|
instance_id: str, *,
|
|
@@ -1075,7 +1186,7 @@ class AsyncTaskHubGrpcClient:
|
|
|
1075
1186
|
resp: pb.QueryInstancesResponse = await self._stub.QueryInstances(req)
|
|
1076
1187
|
if self._payload_store is not None:
|
|
1077
1188
|
await payload_helpers.deexternalize_payloads_async(resp, self._payload_store)
|
|
1078
|
-
states += [parse_orchestration_state(res) for res in resp.orchestrationState]
|
|
1189
|
+
states += [parse_orchestration_state(res, self._data_converter) for res in resp.orchestrationState]
|
|
1079
1190
|
if check_continuation_token(resp.continuationToken, _continuation_token, self._logger):
|
|
1080
1191
|
_continuation_token = resp.continuationToken
|
|
1081
1192
|
else:
|
|
@@ -1095,7 +1206,7 @@ class AsyncTaskHubGrpcClient:
|
|
|
1095
1206
|
)
|
|
1096
1207
|
if self._payload_store is not None and res.exists:
|
|
1097
1208
|
await payload_helpers.deexternalize_payloads_async(res, self._payload_store)
|
|
1098
|
-
return new_orchestration_state(req.instanceId, res)
|
|
1209
|
+
return new_orchestration_state(req.instanceId, res, self._data_converter)
|
|
1099
1210
|
except grpc.aio.AioRpcError as rpc_error:
|
|
1100
1211
|
if rpc_error.code() == grpc.StatusCode.DEADLINE_EXCEEDED:
|
|
1101
1212
|
raise TimeoutError("Timed-out waiting for the orchestration to start")
|
|
@@ -1114,7 +1225,7 @@ class AsyncTaskHubGrpcClient:
|
|
|
1114
1225
|
)
|
|
1115
1226
|
if self._payload_store is not None and res.exists:
|
|
1116
1227
|
await payload_helpers.deexternalize_payloads_async(res, self._payload_store)
|
|
1117
|
-
state = new_orchestration_state(req.instanceId, res)
|
|
1228
|
+
state = new_orchestration_state(req.instanceId, res, self._data_converter)
|
|
1118
1229
|
log_completion_state(self._logger, instance_id, state)
|
|
1119
1230
|
return state
|
|
1120
1231
|
except grpc.aio.AioRpcError as rpc_error:
|
|
@@ -1126,7 +1237,7 @@ class AsyncTaskHubGrpcClient:
|
|
|
1126
1237
|
async def raise_orchestration_event(self, instance_id: str, event_name: str, *,
|
|
1127
1238
|
data: Any | None = None) -> None:
|
|
1128
1239
|
with tracing.start_raise_event_span(event_name, instance_id):
|
|
1129
|
-
req = build_raise_event_req(instance_id, event_name, data)
|
|
1240
|
+
req = build_raise_event_req(instance_id, event_name, data, self._data_converter)
|
|
1130
1241
|
self._logger.info(f"Raising event '{event_name}' for instance '{instance_id}'.")
|
|
1131
1242
|
if self._payload_store is not None:
|
|
1132
1243
|
await payload_helpers.externalize_payloads_async(
|
|
@@ -1137,7 +1248,7 @@ class AsyncTaskHubGrpcClient:
|
|
|
1137
1248
|
async def terminate_orchestration(self, instance_id: str, *,
|
|
1138
1249
|
output: Any | None = None,
|
|
1139
1250
|
recursive: bool = True) -> None:
|
|
1140
|
-
req = build_terminate_req(instance_id, output, recursive)
|
|
1251
|
+
req = build_terminate_req(instance_id, output, recursive, self._data_converter)
|
|
1141
1252
|
|
|
1142
1253
|
self._logger.info(f"Terminating instance '{instance_id}'.")
|
|
1143
1254
|
if self._payload_store is not None:
|
|
@@ -1199,8 +1310,10 @@ class AsyncTaskHubGrpcClient:
|
|
|
1199
1310
|
async def signal_entity(self,
|
|
1200
1311
|
entity_instance_id: EntityInstanceId,
|
|
1201
1312
|
operation_name: str,
|
|
1202
|
-
input: Any | None = None
|
|
1203
|
-
|
|
1313
|
+
input: Any | None = None,
|
|
1314
|
+
signal_time: datetime | None = None) -> None:
|
|
1315
|
+
req = build_signal_entity_req(
|
|
1316
|
+
entity_instance_id, operation_name, input, signal_time, self._data_converter)
|
|
1204
1317
|
self._logger.info(f"Signaling entity '{entity_instance_id}' operation '{operation_name}'.")
|
|
1205
1318
|
if self._payload_store is not None:
|
|
1206
1319
|
await payload_helpers.externalize_payloads_async(
|
|
@@ -1219,7 +1332,7 @@ class AsyncTaskHubGrpcClient:
|
|
|
1219
1332
|
return None
|
|
1220
1333
|
if self._payload_store is not None:
|
|
1221
1334
|
await payload_helpers.deexternalize_payloads_async(res, self._payload_store)
|
|
1222
|
-
return EntityMetadata.from_entity_metadata(res.entity, include_state)
|
|
1335
|
+
return EntityMetadata.from_entity_metadata(res.entity, include_state, self._data_converter)
|
|
1223
1336
|
|
|
1224
1337
|
async def get_all_entities(self,
|
|
1225
1338
|
entity_query: EntityQuery | None = None) -> list[EntityMetadata]:
|
|
@@ -1236,7 +1349,7 @@ class AsyncTaskHubGrpcClient:
|
|
|
1236
1349
|
resp: pb.QueryEntitiesResponse = await self._stub.QueryEntities(query_request)
|
|
1237
1350
|
if self._payload_store is not None:
|
|
1238
1351
|
await payload_helpers.deexternalize_payloads_async(resp, self._payload_store)
|
|
1239
|
-
entities += [EntityMetadata.from_entity_metadata(entity, query_request.query.includeState) for entity in resp.entities]
|
|
1352
|
+
entities += [EntityMetadata.from_entity_metadata(entity, query_request.query.includeState, self._data_converter) for entity in resp.entities]
|
|
1240
1353
|
if check_continuation_token(resp.continuationToken, _continuation_token, self._logger):
|
|
1241
1354
|
_continuation_token = resp.continuationToken
|
|
1242
1355
|
else:
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
1
4
|
from typing import Any, TypeVar, overload
|
|
5
|
+
from datetime import datetime
|
|
2
6
|
|
|
3
7
|
from durabletask.entities.entity_context import EntityContext
|
|
4
8
|
from durabletask.entities.entity_instance_id import EntityInstanceId
|
|
@@ -49,7 +53,9 @@ class DurableEntity:
|
|
|
49
53
|
"""
|
|
50
54
|
self.entity_context.set_state(state)
|
|
51
55
|
|
|
52
|
-
def signal_entity(self, entity_instance_id: EntityInstanceId, operation: str,
|
|
56
|
+
def signal_entity(self, entity_instance_id: EntityInstanceId, operation: str,
|
|
57
|
+
input: Any | None = None,
|
|
58
|
+
signal_time: datetime | None = None) -> None:
|
|
53
59
|
"""Signal another entity to perform an operation.
|
|
54
60
|
|
|
55
61
|
Parameters
|
|
@@ -60,8 +66,12 @@ class DurableEntity:
|
|
|
60
66
|
The operation to perform on the entity.
|
|
61
67
|
input : Any, optional
|
|
62
68
|
The input to provide to the entity for the operation.
|
|
69
|
+
signal_time : datetime, optional
|
|
70
|
+
The time at which the signal should be delivered. If None, the signal is
|
|
71
|
+
delivered as soon as possible. Use this to schedule a future operation,
|
|
72
|
+
for example to have an entity wake itself up at a later time.
|
|
63
73
|
"""
|
|
64
|
-
self.entity_context.signal_entity(entity_instance_id, operation, input)
|
|
74
|
+
self.entity_context.signal_entity(entity_instance_id, operation, input, signal_time)
|
|
65
75
|
|
|
66
76
|
def schedule_new_orchestration(self, orchestration_name: str, input: Any | None = None, instance_id: str | None = None) -> str:
|
|
67
77
|
"""Schedule a new orchestration instance.
|
|
@@ -1,20 +1,29 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
1
3
|
|
|
2
|
-
from
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import TYPE_CHECKING, Any, TypeVar, overload
|
|
3
6
|
import uuid
|
|
7
|
+
from google.protobuf import timestamp_pb2
|
|
4
8
|
from durabletask.entities.entity_instance_id import EntityInstanceId
|
|
5
|
-
from durabletask.internal import helpers
|
|
9
|
+
from durabletask.internal import helpers
|
|
6
10
|
from durabletask.internal.entity_state_shim import StateShim
|
|
7
11
|
import durabletask.internal.orchestrator_service_pb2 as pb
|
|
8
12
|
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from durabletask.serialization import DataConverter
|
|
15
|
+
|
|
9
16
|
TState = TypeVar("TState")
|
|
10
17
|
|
|
11
18
|
|
|
12
19
|
class EntityContext:
|
|
13
|
-
def __init__(self, orchestration_id: str, operation: str, state: StateShim,
|
|
20
|
+
def __init__(self, orchestration_id: str, operation: str, state: StateShim,
|
|
21
|
+
entity_id: EntityInstanceId, data_converter: "DataConverter"):
|
|
14
22
|
self._orchestration_id = orchestration_id
|
|
15
23
|
self._operation = operation
|
|
16
24
|
self._state = state
|
|
17
25
|
self._entity_id = entity_id
|
|
26
|
+
self._data_converter = data_converter
|
|
18
27
|
|
|
19
28
|
@property
|
|
20
29
|
def orchestration_id(self) -> str:
|
|
@@ -81,7 +90,9 @@ class EntityContext:
|
|
|
81
90
|
"""
|
|
82
91
|
self._state.set_state(new_state)
|
|
83
92
|
|
|
84
|
-
def signal_entity(self, entity_instance_id: EntityInstanceId, operation: str,
|
|
93
|
+
def signal_entity(self, entity_instance_id: EntityInstanceId, operation: str,
|
|
94
|
+
input: Any | None = None,
|
|
95
|
+
signal_time: datetime | None = None) -> None:
|
|
85
96
|
"""Signal another entity to perform an operation.
|
|
86
97
|
|
|
87
98
|
Parameters
|
|
@@ -92,15 +103,23 @@ class EntityContext:
|
|
|
92
103
|
The operation to perform on the entity.
|
|
93
104
|
input : Any, optional
|
|
94
105
|
The input to provide to the entity for the operation.
|
|
106
|
+
signal_time : datetime, optional
|
|
107
|
+
The time at which the signal should be delivered. If None, the signal is
|
|
108
|
+
delivered as soon as possible. Use this to schedule a future operation,
|
|
109
|
+
for example to have an entity wake itself up at a later time.
|
|
95
110
|
"""
|
|
96
|
-
encoded_input: str | None =
|
|
111
|
+
encoded_input: str | None = self._data_converter.serialize(input)
|
|
112
|
+
scheduled_time: timestamp_pb2.Timestamp | None = None
|
|
113
|
+
if signal_time is not None:
|
|
114
|
+
scheduled_time = timestamp_pb2.Timestamp()
|
|
115
|
+
scheduled_time.FromDatetime(signal_time)
|
|
97
116
|
self._state.add_operation_action(
|
|
98
117
|
pb.OperationAction(
|
|
99
118
|
sendSignal=pb.SendSignalAction(
|
|
100
119
|
instanceId=str(entity_instance_id),
|
|
101
120
|
name=operation,
|
|
102
121
|
input=helpers.get_string_value(encoded_input),
|
|
103
|
-
scheduledTime=
|
|
122
|
+
scheduledTime=scheduled_time,
|
|
104
123
|
requestTime=None,
|
|
105
124
|
parentTraceContext=None,
|
|
106
125
|
)
|
|
@@ -124,7 +143,7 @@ class EntityContext:
|
|
|
124
143
|
str
|
|
125
144
|
The instance ID of the scheduled orchestration.
|
|
126
145
|
"""
|
|
127
|
-
encoded_input: str | None =
|
|
146
|
+
encoded_input: str | None = self._data_converter.serialize(input)
|
|
128
147
|
if not instance_id:
|
|
129
148
|
instance_id = uuid.uuid4().hex
|
|
130
149
|
self._state.add_operation_action(
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
1
4
|
from datetime import datetime, timezone
|
|
2
|
-
from typing import Any, TypeVar, overload
|
|
5
|
+
from typing import TYPE_CHECKING, Any, TypeVar, overload
|
|
3
6
|
from durabletask.entities.entity_instance_id import EntityInstanceId
|
|
4
7
|
|
|
5
8
|
import durabletask.internal.orchestrator_service_pb2 as pb
|
|
6
9
|
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from durabletask.serialization import DataConverter
|
|
12
|
+
|
|
7
13
|
TState = TypeVar("TState")
|
|
8
14
|
|
|
9
15
|
|
|
@@ -29,7 +35,8 @@ class EntityMetadata:
|
|
|
29
35
|
backlog_queue_size: int,
|
|
30
36
|
locked_by: str,
|
|
31
37
|
includes_state: bool,
|
|
32
|
-
state: Any | None
|
|
38
|
+
state: Any | None,
|
|
39
|
+
data_converter: "DataConverter"):
|
|
33
40
|
"""Initializes a new instance of the EntityMetadata class.
|
|
34
41
|
|
|
35
42
|
Args:
|
|
@@ -41,13 +48,17 @@ class EntityMetadata:
|
|
|
41
48
|
self._locked_by = locked_by
|
|
42
49
|
self.includes_state = includes_state
|
|
43
50
|
self._state = state
|
|
51
|
+
self._data_converter = data_converter
|
|
44
52
|
|
|
45
53
|
@staticmethod
|
|
46
|
-
def from_entity_response(entity_response: pb.GetEntityResponse, includes_state: bool
|
|
47
|
-
|
|
54
|
+
def from_entity_response(entity_response: pb.GetEntityResponse, includes_state: bool,
|
|
55
|
+
data_converter: "DataConverter"):
|
|
56
|
+
return EntityMetadata.from_entity_metadata(
|
|
57
|
+
entity_response.entity, includes_state, data_converter)
|
|
48
58
|
|
|
49
59
|
@staticmethod
|
|
50
|
-
def from_entity_metadata(entity: pb.EntityMetadata, includes_state: bool
|
|
60
|
+
def from_entity_metadata(entity: pb.EntityMetadata, includes_state: bool,
|
|
61
|
+
data_converter: "DataConverter"):
|
|
51
62
|
try:
|
|
52
63
|
entity_id = EntityInstanceId.parse(entity.instanceId)
|
|
53
64
|
except ValueError:
|
|
@@ -61,7 +72,8 @@ class EntityMetadata:
|
|
|
61
72
|
backlog_queue_size=entity.backlogQueueSize,
|
|
62
73
|
locked_by=entity.lockedBy.value,
|
|
63
74
|
includes_state=includes_state,
|
|
64
|
-
state=entity_state
|
|
75
|
+
state=entity_state,
|
|
76
|
+
data_converter=data_converter,
|
|
65
77
|
)
|
|
66
78
|
|
|
67
79
|
@overload
|
|
@@ -73,7 +85,17 @@ class EntityMetadata:
|
|
|
73
85
|
...
|
|
74
86
|
|
|
75
87
|
def get_state(self, intended_type: type[TState] | None = None) -> TState | Any | None:
|
|
76
|
-
"""Get the
|
|
88
|
+
"""Get the entity's raw persisted state, optionally constructor-coerced.
|
|
89
|
+
|
|
90
|
+
The state is held as the raw serialized JSON payload (a ``str``). With no
|
|
91
|
+
argument the raw payload is returned unchanged; passing ``intended_type``
|
|
92
|
+
applies the legacy constructor-based coercion (``intended_type(raw)``)
|
|
93
|
+
and raises ``TypeError`` if that fails.
|
|
94
|
+
|
|
95
|
+
This preserves the pre-existing contract. To deserialize the payload and
|
|
96
|
+
reconstruct dataclasses or ``from_json()``-capable types, use
|
|
97
|
+
:meth:`get_typed_state` instead.
|
|
98
|
+
"""
|
|
77
99
|
if intended_type is None or self._state is None:
|
|
78
100
|
return self._state
|
|
79
101
|
|
|
@@ -87,6 +109,27 @@ class EntityMetadata:
|
|
|
87
109
|
f"Could not convert state of type '{type(self._state).__name__}' to '{intended_type.__name__}'"
|
|
88
110
|
) from ex
|
|
89
111
|
|
|
112
|
+
@overload
|
|
113
|
+
def get_typed_state(self, intended_type: type[TState]) -> TState | None:
|
|
114
|
+
...
|
|
115
|
+
|
|
116
|
+
@overload
|
|
117
|
+
def get_typed_state(self, intended_type: None = None) -> Any:
|
|
118
|
+
...
|
|
119
|
+
|
|
120
|
+
def get_typed_state(self, intended_type: type[TState] | None = None) -> TState | Any | None:
|
|
121
|
+
"""Deserialize the entity's persisted state, optionally reconstructing a type.
|
|
122
|
+
|
|
123
|
+
The state is stored as its raw serialized JSON payload and deserialized
|
|
124
|
+
here. When ``intended_type`` is provided the payload is reconstructed as
|
|
125
|
+
that type (dataclasses, ``from_json()``-capable types, etc.); otherwise
|
|
126
|
+
the plain deserialized JSON value is returned.
|
|
127
|
+
"""
|
|
128
|
+
if self._state is None:
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
return self._data_converter.deserialize(self._state, intended_type)
|
|
132
|
+
|
|
90
133
|
def get_locked_by(self) -> EntityInstanceId | None:
|
|
91
134
|
"""Get the identifier of the worker that currently holds the lock on the entity.
|
|
92
135
|
|