durabletask 1.4.0.dev31__tar.gz → 1.4.0.dev33__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.4.0.dev31 → durabletask-1.4.0.dev33}/PKG-INFO +26 -16
- durabletask-1.4.0.dev33/README.md +47 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/__init__.py +3 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/client.py +41 -18
- durabletask-1.4.0.dev33/durabletask/grpc_options.py +102 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/internal/shared.py +44 -10
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/worker.py +23 -7
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask.egg-info/PKG-INFO +26 -16
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask.egg-info/SOURCES.txt +1 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/pyproject.toml +1 -1
- durabletask-1.4.0.dev31/README.md +0 -37
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/LICENSE +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/entities/__init__.py +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/entities/durable_entity.py +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/entities/entity_context.py +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/entities/entity_instance_id.py +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/entities/entity_lock.py +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/entities/entity_metadata.py +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/entities/entity_operation_failed_exception.py +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/extensions/__init__.py +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/extensions/azure_blob_payloads/__init__.py +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/extensions/azure_blob_payloads/blob_payload_store.py +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/extensions/azure_blob_payloads/options.py +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/history.py +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/internal/client_helpers.py +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/internal/entity_state_shim.py +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/internal/exceptions.py +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/internal/grpc_interceptor.py +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/internal/helpers.py +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/internal/history_helpers.py +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/internal/json_encode_output_exception.py +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/internal/orchestration_entity_context.py +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/internal/orchestrator_service_pb2.py +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/internal/orchestrator_service_pb2.pyi +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/internal/orchestrator_service_pb2_grpc.py +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/internal/proto_task_hub_sidecar_service_stub.py +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/internal/tracing.py +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/payload/__init__.py +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/payload/helpers.py +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/payload/store.py +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/py.typed +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/task.py +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/testing/__init__.py +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/testing/in_memory_backend.py +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask.egg-info/dependency_links.txt +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask.egg-info/requires.txt +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask.egg-info/top_level.txt +0 -0
- {durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: durabletask
|
|
3
|
-
Version: 1.4.0.
|
|
3
|
+
Version: 1.4.0.dev33
|
|
4
4
|
Summary: A Durable Task Client SDK for Python
|
|
5
5
|
License: MIT License
|
|
6
6
|
|
|
@@ -46,38 +46,48 @@ Dynamic: license-file
|
|
|
46
46
|
|
|
47
47
|
# Durable Task SDK for Python
|
|
48
48
|
|
|
49
|
-
[](https://opensource.org/licenses/MIT)
|
|
51
|
+
[](https://github.com/microsoft/durabletask-python/actions/workflows/pr-validation.yml)
|
|
51
53
|
[](https://badge.fury.io/py/durabletask)
|
|
52
54
|
|
|
53
|
-
This repo contains a Python SDK for use with the [Azure Durable Task
|
|
55
|
+
This repo contains a Python SDK for use with the [Azure Durable Task
|
|
56
|
+
Scheduler](https://github.com/Azure/Durable-Task-Scheduler). With this SDK, you can define,
|
|
57
|
+
schedule, and manage durable orchestrations using ordinary Python code.
|
|
54
58
|
|
|
55
|
-
> Note that this SDK is **not** currently compatible with [Azure Durable
|
|
59
|
+
> Note that this SDK is **not** currently compatible with [Azure Durable
|
|
60
|
+
Functions](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-overview). If
|
|
61
|
+
you are looking for a Python SDK for Azure Durable Functions, please see [this
|
|
62
|
+
repo](https://github.com/Azure/azure-functions-durable-python).
|
|
63
|
+
|
|
64
|
+
## References
|
|
56
65
|
|
|
57
|
-
# References
|
|
58
66
|
- [Supported Patterns](./docs/supported-patterns.md)
|
|
59
67
|
- [Available Features](./docs/features.md)
|
|
60
68
|
- [Getting Started](./docs/getting-started.md)
|
|
61
|
-
- [Development Guide](./docs/development.md)
|
|
69
|
+
- [Development Guide](./docs/development.md)
|
|
62
70
|
- [Contributing Guide](./CONTRIBUTING.md)
|
|
63
71
|
|
|
64
72
|
## Optional Features
|
|
65
73
|
|
|
66
74
|
### Large Payload Externalization
|
|
67
75
|
|
|
68
|
-
Install the `azure-blob-payloads` extra to automatically offload
|
|
69
|
-
|
|
76
|
+
Install the `azure-blob-payloads` extra to automatically offload oversized orchestration payloads to
|
|
77
|
+
Azure Blob Storage:
|
|
70
78
|
|
|
71
79
|
```bash
|
|
72
80
|
pip install durabletask[azure-blob-payloads]
|
|
73
81
|
```
|
|
74
82
|
|
|
75
|
-
See the [feature documentation](./docs/features.md#large-payload-externalization)
|
|
76
|
-
|
|
83
|
+
See the [feature documentation](./docs/features.md#large-payload-externalization) and the
|
|
84
|
+
[example](./examples/large_payload/) for usage details.
|
|
77
85
|
|
|
78
86
|
## Trademarks
|
|
79
|
-
|
|
80
|
-
trademarks or logos
|
|
81
|
-
[Microsoft's Trademark & Brand
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
|
|
88
|
+
This project may contain trademarks or logos for projects, products, or services. Authorized use of
|
|
89
|
+
Microsoft trademarks or logos is subject to and must follow [Microsoft's Trademark & Brand
|
|
90
|
+
Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general).
|
|
91
|
+
Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion
|
|
92
|
+
or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those
|
|
93
|
+
third-party's policies.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Durable Task SDK for Python
|
|
2
|
+
|
|
3
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://github.com/microsoft/durabletask-python/actions/workflows/pr-validation.yml)
|
|
7
|
+
[](https://badge.fury.io/py/durabletask)
|
|
8
|
+
|
|
9
|
+
This repo contains a Python SDK for use with the [Azure Durable Task
|
|
10
|
+
Scheduler](https://github.com/Azure/Durable-Task-Scheduler). With this SDK, you can define,
|
|
11
|
+
schedule, and manage durable orchestrations using ordinary Python code.
|
|
12
|
+
|
|
13
|
+
> Note that this SDK is **not** currently compatible with [Azure Durable
|
|
14
|
+
Functions](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-overview). If
|
|
15
|
+
you are looking for a Python SDK for Azure Durable Functions, please see [this
|
|
16
|
+
repo](https://github.com/Azure/azure-functions-durable-python).
|
|
17
|
+
|
|
18
|
+
## References
|
|
19
|
+
|
|
20
|
+
- [Supported Patterns](./docs/supported-patterns.md)
|
|
21
|
+
- [Available Features](./docs/features.md)
|
|
22
|
+
- [Getting Started](./docs/getting-started.md)
|
|
23
|
+
- [Development Guide](./docs/development.md)
|
|
24
|
+
- [Contributing Guide](./CONTRIBUTING.md)
|
|
25
|
+
|
|
26
|
+
## Optional Features
|
|
27
|
+
|
|
28
|
+
### Large Payload Externalization
|
|
29
|
+
|
|
30
|
+
Install the `azure-blob-payloads` extra to automatically offload oversized orchestration payloads to
|
|
31
|
+
Azure Blob Storage:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install durabletask[azure-blob-payloads]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
See the [feature documentation](./docs/features.md#large-payload-externalization) and the
|
|
38
|
+
[example](./examples/large_payload/) for usage details.
|
|
39
|
+
|
|
40
|
+
## Trademarks
|
|
41
|
+
|
|
42
|
+
This project may contain trademarks or logos for projects, products, or services. Authorized use of
|
|
43
|
+
Microsoft trademarks or logos is subject to and must follow [Microsoft's Trademark & Brand
|
|
44
|
+
Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general).
|
|
45
|
+
Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion
|
|
46
|
+
or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those
|
|
47
|
+
third-party's policies.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
"""Durable Task SDK for Python"""
|
|
5
5
|
|
|
6
|
+
from durabletask.grpc_options import GrpcChannelOptions, GrpcRetryPolicyOptions
|
|
6
7
|
from durabletask.payload.store import LargePayloadStorageOptions, PayloadStore
|
|
7
8
|
from durabletask.worker import (
|
|
8
9
|
ActivityWorkItemFilter,
|
|
@@ -17,6 +18,8 @@ __all__ = [
|
|
|
17
18
|
"ActivityWorkItemFilter",
|
|
18
19
|
"ConcurrencyOptions",
|
|
19
20
|
"EntityWorkItemFilter",
|
|
21
|
+
"GrpcChannelOptions",
|
|
22
|
+
"GrpcRetryPolicyOptions",
|
|
20
23
|
"LargePayloadStorageOptions",
|
|
21
24
|
"OrchestrationWorkItemFilter",
|
|
22
25
|
"PayloadStore",
|
|
@@ -14,6 +14,7 @@ import grpc.aio
|
|
|
14
14
|
import durabletask.history as history
|
|
15
15
|
from durabletask.entities import EntityInstanceId
|
|
16
16
|
from durabletask.entities.entity_metadata import EntityMetadata
|
|
17
|
+
from durabletask.grpc_options import GrpcChannelOptions
|
|
17
18
|
import durabletask.internal.helpers as helpers
|
|
18
19
|
import durabletask.internal.history_helpers as history_helpers
|
|
19
20
|
import durabletask.internal.orchestrator_service_pb2 as pb
|
|
@@ -161,18 +162,22 @@ class TaskHubGrpcClient:
|
|
|
161
162
|
metadata: Optional[list[tuple[str, str]]] = None,
|
|
162
163
|
log_handler: Optional[logging.Handler] = None,
|
|
163
164
|
log_formatter: Optional[logging.Formatter] = None,
|
|
165
|
+
channel: Optional[grpc.Channel] = None,
|
|
164
166
|
secure_channel: bool = False,
|
|
165
167
|
interceptors: Optional[Sequence[shared.ClientInterceptor]] = None,
|
|
168
|
+
channel_options: Optional[GrpcChannelOptions] = None,
|
|
166
169
|
default_version: Optional[str] = None,
|
|
167
170
|
payload_store: Optional[PayloadStore] = None):
|
|
168
171
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
172
|
+
self._owns_channel = channel is None
|
|
173
|
+
if channel is None:
|
|
174
|
+
interceptors = prepare_sync_interceptors(metadata, interceptors)
|
|
175
|
+
channel = shared.get_grpc_channel(
|
|
176
|
+
host_address=host_address,
|
|
177
|
+
secure_channel=secure_channel,
|
|
178
|
+
interceptors=interceptors,
|
|
179
|
+
channel_options=channel_options,
|
|
180
|
+
)
|
|
176
181
|
self._channel = channel
|
|
177
182
|
self._stub = stubs.TaskHubSidecarServiceStub(channel)
|
|
178
183
|
self._logger = shared.get_logger("client", log_handler, log_formatter)
|
|
@@ -180,8 +185,15 @@ class TaskHubGrpcClient:
|
|
|
180
185
|
self._payload_store = payload_store
|
|
181
186
|
|
|
182
187
|
def close(self) -> None:
|
|
183
|
-
"""Closes the underlying gRPC channel.
|
|
184
|
-
|
|
188
|
+
"""Closes the underlying gRPC channel.
|
|
189
|
+
|
|
190
|
+
Only closes channels created internally. If a pre-configured channel
|
|
191
|
+
was passed via the ``channel`` constructor parameter, this method is
|
|
192
|
+
a no-op — the caller retains ownership and is responsible for closing
|
|
193
|
+
it.
|
|
194
|
+
"""
|
|
195
|
+
if self._owns_channel:
|
|
196
|
+
self._channel.close()
|
|
185
197
|
|
|
186
198
|
def schedule_new_orchestration(self, orchestrator: Union[task.Orchestrator[TInput, TOutput], str], *,
|
|
187
199
|
input: Optional[TInput] = None,
|
|
@@ -480,18 +492,22 @@ class AsyncTaskHubGrpcClient:
|
|
|
480
492
|
metadata: Optional[list[tuple[str, str]]] = None,
|
|
481
493
|
log_handler: Optional[logging.Handler] = None,
|
|
482
494
|
log_formatter: Optional[logging.Formatter] = None,
|
|
495
|
+
channel: Optional[grpc.aio.Channel] = None,
|
|
483
496
|
secure_channel: bool = False,
|
|
484
497
|
interceptors: Optional[Sequence[shared.AsyncClientInterceptor]] = None,
|
|
498
|
+
channel_options: Optional[GrpcChannelOptions] = None,
|
|
485
499
|
default_version: Optional[str] = None,
|
|
486
500
|
payload_store: Optional[PayloadStore] = None):
|
|
487
501
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
502
|
+
self._owns_channel = channel is None
|
|
503
|
+
if channel is None:
|
|
504
|
+
interceptors = prepare_async_interceptors(metadata, interceptors)
|
|
505
|
+
channel = shared.get_async_grpc_channel(
|
|
506
|
+
host_address=host_address,
|
|
507
|
+
secure_channel=secure_channel,
|
|
508
|
+
interceptors=interceptors,
|
|
509
|
+
channel_options=channel_options,
|
|
510
|
+
)
|
|
495
511
|
self._channel = channel
|
|
496
512
|
self._stub = stubs.TaskHubSidecarServiceStub(channel)
|
|
497
513
|
self._logger = shared.get_logger("async_client", log_handler, log_formatter)
|
|
@@ -499,8 +515,15 @@ class AsyncTaskHubGrpcClient:
|
|
|
499
515
|
self._payload_store = payload_store
|
|
500
516
|
|
|
501
517
|
async def close(self) -> None:
|
|
502
|
-
"""Closes the underlying gRPC channel.
|
|
503
|
-
|
|
518
|
+
"""Closes the underlying gRPC channel.
|
|
519
|
+
|
|
520
|
+
Only closes channels created internally. If a pre-configured channel
|
|
521
|
+
was passed via the ``channel`` constructor parameter, this method is
|
|
522
|
+
a no-op — the caller retains ownership and is responsible for closing
|
|
523
|
+
it.
|
|
524
|
+
"""
|
|
525
|
+
if self._owns_channel:
|
|
526
|
+
await self._channel.close()
|
|
504
527
|
|
|
505
528
|
async def __aenter__(self):
|
|
506
529
|
return self
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
import json
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class GrpcRetryPolicyOptions:
|
|
13
|
+
"""Configuration for transport-level gRPC retries."""
|
|
14
|
+
|
|
15
|
+
max_attempts: int = 4
|
|
16
|
+
initial_backoff_seconds: float = 0.05
|
|
17
|
+
max_backoff_seconds: float = 0.25
|
|
18
|
+
backoff_multiplier: float = 2.0
|
|
19
|
+
retryable_status_codes: list[str] = field(default_factory=lambda: ["UNAVAILABLE"])
|
|
20
|
+
|
|
21
|
+
def __post_init__(self) -> None:
|
|
22
|
+
if self.max_attempts < 2:
|
|
23
|
+
raise ValueError("max_attempts must be >= 2")
|
|
24
|
+
if self.initial_backoff_seconds <= 0:
|
|
25
|
+
raise ValueError("initial_backoff_seconds must be > 0")
|
|
26
|
+
if self.max_backoff_seconds <= 0:
|
|
27
|
+
raise ValueError("max_backoff_seconds must be > 0")
|
|
28
|
+
if self.backoff_multiplier <= 0:
|
|
29
|
+
raise ValueError("backoff_multiplier must be > 0")
|
|
30
|
+
if self.max_backoff_seconds < self.initial_backoff_seconds:
|
|
31
|
+
raise ValueError("max_backoff_seconds must be >= initial_backoff_seconds")
|
|
32
|
+
if len(self.retryable_status_codes) == 0:
|
|
33
|
+
raise ValueError("retryable_status_codes cannot be empty")
|
|
34
|
+
# Validate that backoff values are representable as non-zero gRPC duration strings.
|
|
35
|
+
self._format_duration(self.initial_backoff_seconds)
|
|
36
|
+
self._format_duration(self.max_backoff_seconds)
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def _format_duration(seconds: float) -> str:
|
|
40
|
+
formatted = f"{seconds:.9f}".rstrip('0')
|
|
41
|
+
if formatted.endswith('.'):
|
|
42
|
+
formatted += '0'
|
|
43
|
+
if float(formatted) == 0:
|
|
44
|
+
raise ValueError(
|
|
45
|
+
f"Duration {seconds!r} rounds to zero; use a value large enough to "
|
|
46
|
+
"produce a non-zero gRPC duration string."
|
|
47
|
+
)
|
|
48
|
+
return f"{formatted}s"
|
|
49
|
+
|
|
50
|
+
def to_service_config(self) -> dict[str, Any]:
|
|
51
|
+
return {
|
|
52
|
+
"methodConfig": [
|
|
53
|
+
{
|
|
54
|
+
"name": [{}],
|
|
55
|
+
"retryPolicy": {
|
|
56
|
+
"maxAttempts": self.max_attempts,
|
|
57
|
+
"initialBackoff": self._format_duration(self.initial_backoff_seconds),
|
|
58
|
+
"maxBackoff": self._format_duration(self.max_backoff_seconds),
|
|
59
|
+
"backoffMultiplier": self.backoff_multiplier,
|
|
60
|
+
"retryableStatusCodes": self.retryable_status_codes,
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class GrpcChannelOptions:
|
|
69
|
+
"""Configuration for transport-level gRPC channel behavior."""
|
|
70
|
+
|
|
71
|
+
max_receive_message_length: Optional[int] = None
|
|
72
|
+
max_send_message_length: Optional[int] = None
|
|
73
|
+
keepalive_time_ms: Optional[int] = None
|
|
74
|
+
keepalive_timeout_ms: Optional[int] = None
|
|
75
|
+
keepalive_permit_without_calls: Optional[bool] = None
|
|
76
|
+
retry_policy: Optional[GrpcRetryPolicyOptions] = None
|
|
77
|
+
raw_options: list[tuple[str, Any]] = field(default_factory=list)
|
|
78
|
+
|
|
79
|
+
def to_grpc_options(self) -> list[tuple[str, Any]]:
|
|
80
|
+
options = list(self.raw_options)
|
|
81
|
+
|
|
82
|
+
if self.max_receive_message_length is not None:
|
|
83
|
+
options.append(("grpc.max_receive_message_length", self.max_receive_message_length))
|
|
84
|
+
if self.max_send_message_length is not None:
|
|
85
|
+
options.append(("grpc.max_send_message_length", self.max_send_message_length))
|
|
86
|
+
if self.keepalive_time_ms is not None:
|
|
87
|
+
options.append(("grpc.keepalive_time_ms", self.keepalive_time_ms))
|
|
88
|
+
if self.keepalive_timeout_ms is not None:
|
|
89
|
+
options.append(("grpc.keepalive_timeout_ms", self.keepalive_timeout_ms))
|
|
90
|
+
if self.keepalive_permit_without_calls is not None:
|
|
91
|
+
options.append(
|
|
92
|
+
(
|
|
93
|
+
"grpc.keepalive_permit_without_calls",
|
|
94
|
+
1 if self.keepalive_permit_without_calls else 0,
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if self.retry_policy is not None:
|
|
99
|
+
options.append(("grpc.enable_retries", 1))
|
|
100
|
+
options.append(("grpc.service_config", json.dumps(self.retry_policy.to_service_config())))
|
|
101
|
+
|
|
102
|
+
return options
|
|
@@ -9,6 +9,7 @@ from typing import Any, Optional, Sequence, Union
|
|
|
9
9
|
|
|
10
10
|
import grpc
|
|
11
11
|
import grpc.aio
|
|
12
|
+
from durabletask.grpc_options import GrpcChannelOptions
|
|
12
13
|
|
|
13
14
|
ClientInterceptor = Union[
|
|
14
15
|
grpc.UnaryUnaryClientInterceptor,
|
|
@@ -39,7 +40,8 @@ def get_default_host_address() -> str:
|
|
|
39
40
|
def get_grpc_channel(
|
|
40
41
|
host_address: Optional[str],
|
|
41
42
|
secure_channel: bool = False,
|
|
42
|
-
interceptors: Optional[Sequence[ClientInterceptor]] = None
|
|
43
|
+
interceptors: Optional[Sequence[ClientInterceptor]] = None,
|
|
44
|
+
channel_options: Optional[GrpcChannelOptions] = None) -> grpc.Channel:
|
|
43
45
|
|
|
44
46
|
if host_address is None:
|
|
45
47
|
host_address = get_default_host_address()
|
|
@@ -59,10 +61,21 @@ def get_grpc_channel(
|
|
|
59
61
|
break
|
|
60
62
|
|
|
61
63
|
# Create the base channel
|
|
64
|
+
options = channel_options.to_grpc_options() if channel_options is not None else None
|
|
62
65
|
if secure_channel:
|
|
63
|
-
|
|
66
|
+
if options is None:
|
|
67
|
+
channel = grpc.secure_channel(host_address, grpc.ssl_channel_credentials())
|
|
68
|
+
else:
|
|
69
|
+
channel = grpc.secure_channel(
|
|
70
|
+
host_address,
|
|
71
|
+
grpc.ssl_channel_credentials(),
|
|
72
|
+
options=options,
|
|
73
|
+
)
|
|
64
74
|
else:
|
|
65
|
-
|
|
75
|
+
if options is None:
|
|
76
|
+
channel = grpc.insecure_channel(host_address)
|
|
77
|
+
else:
|
|
78
|
+
channel = grpc.insecure_channel(host_address, options=options)
|
|
66
79
|
|
|
67
80
|
# Apply interceptors ONLY if they exist
|
|
68
81
|
if interceptors:
|
|
@@ -73,7 +86,8 @@ def get_grpc_channel(
|
|
|
73
86
|
def get_async_grpc_channel(
|
|
74
87
|
host_address: Optional[str],
|
|
75
88
|
secure_channel: bool = False,
|
|
76
|
-
interceptors: Optional[Sequence[AsyncClientInterceptor]] = None
|
|
89
|
+
interceptors: Optional[Sequence[AsyncClientInterceptor]] = None,
|
|
90
|
+
channel_options: Optional[GrpcChannelOptions] = None) -> grpc.aio.Channel:
|
|
77
91
|
|
|
78
92
|
if host_address is None:
|
|
79
93
|
host_address = get_default_host_address()
|
|
@@ -90,14 +104,34 @@ def get_async_grpc_channel(
|
|
|
90
104
|
host_address = host_address[len(protocol):]
|
|
91
105
|
break
|
|
92
106
|
|
|
107
|
+
options = channel_options.to_grpc_options() if channel_options is not None else None
|
|
108
|
+
|
|
93
109
|
if secure_channel:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
110
|
+
if options is None:
|
|
111
|
+
channel = grpc.aio.secure_channel(
|
|
112
|
+
host_address,
|
|
113
|
+
grpc.ssl_channel_credentials(),
|
|
114
|
+
interceptors=interceptors,
|
|
115
|
+
)
|
|
116
|
+
else:
|
|
117
|
+
channel = grpc.aio.secure_channel(
|
|
118
|
+
host_address,
|
|
119
|
+
grpc.ssl_channel_credentials(),
|
|
120
|
+
interceptors=interceptors,
|
|
121
|
+
options=options,
|
|
122
|
+
)
|
|
97
123
|
else:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
124
|
+
if options is None:
|
|
125
|
+
channel = grpc.aio.insecure_channel(
|
|
126
|
+
host_address,
|
|
127
|
+
interceptors=interceptors,
|
|
128
|
+
)
|
|
129
|
+
else:
|
|
130
|
+
channel = grpc.aio.insecure_channel(
|
|
131
|
+
host_address,
|
|
132
|
+
interceptors=interceptors,
|
|
133
|
+
options=options,
|
|
134
|
+
)
|
|
101
135
|
|
|
102
136
|
return channel
|
|
103
137
|
|
|
@@ -21,6 +21,7 @@ from packaging.version import InvalidVersion, parse
|
|
|
21
21
|
import grpc
|
|
22
22
|
from google.protobuf import empty_pb2
|
|
23
23
|
|
|
24
|
+
from durabletask.grpc_options import GrpcChannelOptions
|
|
24
25
|
from durabletask.entities.entity_operation_failed_exception import EntityOperationFailedException
|
|
25
26
|
from durabletask.internal import helpers
|
|
26
27
|
from durabletask.internal.entity_state_shim import StateShim
|
|
@@ -361,8 +362,13 @@ class TaskHubGrpcWorker:
|
|
|
361
362
|
Defaults to None.
|
|
362
363
|
secure_channel (bool, optional): Whether to use a secure gRPC channel (TLS).
|
|
363
364
|
Defaults to False.
|
|
365
|
+
channel (Optional[grpc.Channel], optional): Pre-configured gRPC channel to use.
|
|
366
|
+
If set, host address, secure_channel, interceptors, and channel_options
|
|
367
|
+
are ignored when creating connections.
|
|
364
368
|
interceptors (Optional[Sequence[shared.ClientInterceptor]], optional): Custom gRPC
|
|
365
369
|
interceptors to apply to the channel. Defaults to None.
|
|
370
|
+
channel_options (Optional[GrpcChannelOptions], optional): Extra low-level gRPC
|
|
371
|
+
channel configuration including retry/service config options.
|
|
366
372
|
concurrency_options (Optional[ConcurrencyOptions], optional): Configuration for
|
|
367
373
|
controlling worker concurrency limits. If None, default settings are used.
|
|
368
374
|
|
|
@@ -426,8 +432,10 @@ class TaskHubGrpcWorker:
|
|
|
426
432
|
metadata: Optional[list[tuple[str, str]]] = None,
|
|
427
433
|
log_handler: Optional[logging.Handler] = None,
|
|
428
434
|
log_formatter: Optional[logging.Formatter] = None,
|
|
435
|
+
channel: Optional[grpc.Channel] = None,
|
|
429
436
|
secure_channel: bool = False,
|
|
430
437
|
interceptors: Optional[Sequence[shared.ClientInterceptor]] = None,
|
|
438
|
+
channel_options: Optional[GrpcChannelOptions] = None,
|
|
431
439
|
concurrency_options: Optional[ConcurrencyOptions] = None,
|
|
432
440
|
maximum_timer_interval: Optional[timedelta] = DEFAULT_MAXIMUM_TIMER_INTERVAL,
|
|
433
441
|
payload_store: Optional[PayloadStore] = None,
|
|
@@ -439,8 +447,10 @@ class TaskHubGrpcWorker:
|
|
|
439
447
|
self._logger = shared.get_logger("worker", log_handler, log_formatter)
|
|
440
448
|
self._shutdown = Event()
|
|
441
449
|
self._is_running = False
|
|
450
|
+
self._channel = channel
|
|
442
451
|
self._secure_channel = secure_channel
|
|
443
452
|
self._payload_store = payload_store
|
|
453
|
+
self._channel_options = channel_options
|
|
444
454
|
|
|
445
455
|
# Use provided concurrency options or create default ones
|
|
446
456
|
self._concurrency_options = (
|
|
@@ -590,7 +600,7 @@ class TaskHubGrpcWorker:
|
|
|
590
600
|
|
|
591
601
|
def create_fresh_connection():
|
|
592
602
|
nonlocal current_channel, current_stub, conn_retry_count
|
|
593
|
-
if current_channel:
|
|
603
|
+
if current_channel and self._channel is None:
|
|
594
604
|
try:
|
|
595
605
|
current_channel.close()
|
|
596
606
|
except Exception:
|
|
@@ -598,16 +608,22 @@ class TaskHubGrpcWorker:
|
|
|
598
608
|
current_channel = None
|
|
599
609
|
current_stub = None
|
|
600
610
|
try:
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
611
|
+
if self._channel is not None:
|
|
612
|
+
current_channel = self._channel
|
|
613
|
+
else:
|
|
614
|
+
current_channel = shared.get_grpc_channel(
|
|
615
|
+
self._host_address,
|
|
616
|
+
self._secure_channel,
|
|
617
|
+
self._interceptors,
|
|
618
|
+
channel_options=self._channel_options,
|
|
619
|
+
)
|
|
604
620
|
current_stub = stubs.TaskHubSidecarServiceStub(current_channel)
|
|
605
621
|
current_stub.Hello(empty_pb2.Empty())
|
|
606
622
|
conn_retry_count = 0
|
|
607
623
|
self._logger.info(f"Created fresh connection to {self._host_address}")
|
|
608
624
|
except Exception as e:
|
|
609
625
|
self._logger.warning(f"Failed to create connection: {e}")
|
|
610
|
-
current_channel = None
|
|
626
|
+
current_channel = self._channel if self._channel is not None else None
|
|
611
627
|
current_stub = None
|
|
612
628
|
raise
|
|
613
629
|
|
|
@@ -632,12 +648,12 @@ class TaskHubGrpcWorker:
|
|
|
632
648
|
current_reader_thread = None
|
|
633
649
|
|
|
634
650
|
# Close the channel
|
|
635
|
-
if current_channel:
|
|
651
|
+
if current_channel and self._channel is None:
|
|
636
652
|
try:
|
|
637
653
|
current_channel.close()
|
|
638
654
|
except Exception:
|
|
639
655
|
pass
|
|
640
|
-
current_channel = None
|
|
656
|
+
current_channel = self._channel if self._channel is not None else None
|
|
641
657
|
current_stub = None
|
|
642
658
|
|
|
643
659
|
def should_invalidate_connection(rpc_error):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: durabletask
|
|
3
|
-
Version: 1.4.0.
|
|
3
|
+
Version: 1.4.0.dev33
|
|
4
4
|
Summary: A Durable Task Client SDK for Python
|
|
5
5
|
License: MIT License
|
|
6
6
|
|
|
@@ -46,38 +46,48 @@ Dynamic: license-file
|
|
|
46
46
|
|
|
47
47
|
# Durable Task SDK for Python
|
|
48
48
|
|
|
49
|
-
[](https://opensource.org/licenses/MIT)
|
|
51
|
+
[](https://github.com/microsoft/durabletask-python/actions/workflows/pr-validation.yml)
|
|
51
53
|
[](https://badge.fury.io/py/durabletask)
|
|
52
54
|
|
|
53
|
-
This repo contains a Python SDK for use with the [Azure Durable Task
|
|
55
|
+
This repo contains a Python SDK for use with the [Azure Durable Task
|
|
56
|
+
Scheduler](https://github.com/Azure/Durable-Task-Scheduler). With this SDK, you can define,
|
|
57
|
+
schedule, and manage durable orchestrations using ordinary Python code.
|
|
54
58
|
|
|
55
|
-
> Note that this SDK is **not** currently compatible with [Azure Durable
|
|
59
|
+
> Note that this SDK is **not** currently compatible with [Azure Durable
|
|
60
|
+
Functions](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-overview). If
|
|
61
|
+
you are looking for a Python SDK for Azure Durable Functions, please see [this
|
|
62
|
+
repo](https://github.com/Azure/azure-functions-durable-python).
|
|
63
|
+
|
|
64
|
+
## References
|
|
56
65
|
|
|
57
|
-
# References
|
|
58
66
|
- [Supported Patterns](./docs/supported-patterns.md)
|
|
59
67
|
- [Available Features](./docs/features.md)
|
|
60
68
|
- [Getting Started](./docs/getting-started.md)
|
|
61
|
-
- [Development Guide](./docs/development.md)
|
|
69
|
+
- [Development Guide](./docs/development.md)
|
|
62
70
|
- [Contributing Guide](./CONTRIBUTING.md)
|
|
63
71
|
|
|
64
72
|
## Optional Features
|
|
65
73
|
|
|
66
74
|
### Large Payload Externalization
|
|
67
75
|
|
|
68
|
-
Install the `azure-blob-payloads` extra to automatically offload
|
|
69
|
-
|
|
76
|
+
Install the `azure-blob-payloads` extra to automatically offload oversized orchestration payloads to
|
|
77
|
+
Azure Blob Storage:
|
|
70
78
|
|
|
71
79
|
```bash
|
|
72
80
|
pip install durabletask[azure-blob-payloads]
|
|
73
81
|
```
|
|
74
82
|
|
|
75
|
-
See the [feature documentation](./docs/features.md#large-payload-externalization)
|
|
76
|
-
|
|
83
|
+
See the [feature documentation](./docs/features.md#large-payload-externalization) and the
|
|
84
|
+
[example](./examples/large_payload/) for usage details.
|
|
77
85
|
|
|
78
86
|
## Trademarks
|
|
79
|
-
|
|
80
|
-
trademarks or logos
|
|
81
|
-
[Microsoft's Trademark & Brand
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
|
|
88
|
+
This project may contain trademarks or logos for projects, products, or services. Authorized use of
|
|
89
|
+
Microsoft trademarks or logos is subject to and must follow [Microsoft's Trademark & Brand
|
|
90
|
+
Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general).
|
|
91
|
+
Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion
|
|
92
|
+
or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those
|
|
93
|
+
third-party's policies.
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# Durable Task SDK for Python
|
|
2
|
-
|
|
3
|
-
[](https://opensource.org/licenses/MIT)
|
|
4
|
-
[](https://github.com/microsoft/durabletask-python/actions/workflows/pr-validation.yml)
|
|
5
|
-
[](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
|
-
## Optional Features
|
|
19
|
-
|
|
20
|
-
### Large Payload Externalization
|
|
21
|
-
|
|
22
|
-
Install the `azure-blob-payloads` extra to automatically offload
|
|
23
|
-
oversized orchestration payloads to Azure Blob Storage:
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
pip install durabletask[azure-blob-payloads]
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
See the [feature documentation](./docs/features.md#large-payload-externalization)
|
|
30
|
-
and the [example](./examples/large_payload/) for usage details.
|
|
31
|
-
|
|
32
|
-
## Trademarks
|
|
33
|
-
This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft
|
|
34
|
-
trademarks or logos is subject to and must follow
|
|
35
|
-
[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general).
|
|
36
|
-
Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.
|
|
37
|
-
Any use of third-party trademarks or logos are subject to those third-party's policies.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/entities/entity_instance_id.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/internal/entity_state_shim.py
RENAMED
|
File without changes
|
|
File without changes
|
{durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/internal/grpc_interceptor.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/internal/orchestrator_service_pb2.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask/testing/in_memory_backend.py
RENAMED
|
File without changes
|
{durabletask-1.4.0.dev31 → durabletask-1.4.0.dev33}/durabletask.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|