durabletask.azuremanaged 0.0.0.dev69__tar.gz → 0.0.0.dev71__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_azuremanaged-0.0.0.dev69 → durabletask_azuremanaged-0.0.0.dev71}/PKG-INFO +3 -1
- durabletask_azuremanaged-0.0.0.dev71/durabletask/azuremanaged/client.py +141 -0
- {durabletask_azuremanaged-0.0.0.dev69 → durabletask_azuremanaged-0.0.0.dev71}/durabletask/azuremanaged/internal/access_token_manager.py +43 -1
- durabletask_azuremanaged-0.0.0.dev71/durabletask/azuremanaged/internal/durabletask_grpc_interceptor.py +129 -0
- {durabletask_azuremanaged-0.0.0.dev69 → durabletask_azuremanaged-0.0.0.dev71}/durabletask/azuremanaged/worker.py +36 -4
- {durabletask_azuremanaged-0.0.0.dev69 → durabletask_azuremanaged-0.0.0.dev71}/durabletask.azuremanaged.egg-info/PKG-INFO +3 -1
- durabletask_azuremanaged-0.0.0.dev71/durabletask.azuremanaged.egg-info/requires.txt +5 -0
- {durabletask_azuremanaged-0.0.0.dev69 → durabletask_azuremanaged-0.0.0.dev71}/pyproject.toml +6 -1
- durabletask_azuremanaged-0.0.0.dev69/durabletask/azuremanaged/client.py +0 -41
- durabletask_azuremanaged-0.0.0.dev69/durabletask/azuremanaged/internal/durabletask_grpc_interceptor.py +0 -54
- durabletask_azuremanaged-0.0.0.dev69/durabletask.azuremanaged.egg-info/requires.txt +0 -2
- {durabletask_azuremanaged-0.0.0.dev69 → durabletask_azuremanaged-0.0.0.dev71}/durabletask/azuremanaged/__init__.py +0 -0
- {durabletask_azuremanaged-0.0.0.dev69 → durabletask_azuremanaged-0.0.0.dev71}/durabletask/azuremanaged/internal/py.typed +0 -0
- {durabletask_azuremanaged-0.0.0.dev69 → durabletask_azuremanaged-0.0.0.dev71}/durabletask/azuremanaged/py.typed +0 -0
- {durabletask_azuremanaged-0.0.0.dev69 → durabletask_azuremanaged-0.0.0.dev71}/durabletask.azuremanaged.egg-info/SOURCES.txt +0 -0
- {durabletask_azuremanaged-0.0.0.dev69 → durabletask_azuremanaged-0.0.0.dev71}/durabletask.azuremanaged.egg-info/dependency_links.txt +0 -0
- {durabletask_azuremanaged-0.0.0.dev69 → durabletask_azuremanaged-0.0.0.dev71}/durabletask.azuremanaged.egg-info/top_level.txt +0 -0
- {durabletask_azuremanaged-0.0.0.dev69 → durabletask_azuremanaged-0.0.0.dev71}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: durabletask.azuremanaged
|
|
3
|
-
Version: 0.0.0.
|
|
3
|
+
Version: 0.0.0.dev71
|
|
4
4
|
Summary: Durable Task Python SDK provider implementation for the Azure Durable Task Scheduler
|
|
5
5
|
Project-URL: repository, https://github.com/microsoft/durabletask-python
|
|
6
6
|
Project-URL: changelog, https://github.com/microsoft/durabletask-python/blob/main/CHANGELOG.md
|
|
@@ -12,3 +12,5 @@ Requires-Python: >=3.10
|
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
Requires-Dist: durabletask>=0.0.0dev1
|
|
14
14
|
Requires-Dist: azure-identity>=1.19.0
|
|
15
|
+
Provides-Extra: azure-blob-payloads
|
|
16
|
+
Requires-Dist: durabletask[azure-blob-payloads]>=1.4.0; extra == "azure-blob-payloads"
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from typing import Optional, Sequence
|
|
7
|
+
|
|
8
|
+
import grpc
|
|
9
|
+
import grpc.aio
|
|
10
|
+
from azure.core.credentials import TokenCredential
|
|
11
|
+
from azure.core.credentials_async import AsyncTokenCredential
|
|
12
|
+
|
|
13
|
+
from durabletask.azuremanaged.internal.durabletask_grpc_interceptor import (
|
|
14
|
+
DTSAsyncDefaultClientInterceptorImpl,
|
|
15
|
+
DTSDefaultClientInterceptorImpl,
|
|
16
|
+
)
|
|
17
|
+
from durabletask.client import AsyncTaskHubGrpcClient, TaskHubGrpcClient
|
|
18
|
+
from durabletask.grpc_options import (
|
|
19
|
+
GrpcChannelOptions,
|
|
20
|
+
GrpcClientResiliencyOptions,
|
|
21
|
+
)
|
|
22
|
+
import durabletask.internal.shared as shared
|
|
23
|
+
from durabletask.payload.store import PayloadStore
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Client class used for Durable Task Scheduler (DTS)
|
|
27
|
+
class DurableTaskSchedulerClient(TaskHubGrpcClient):
|
|
28
|
+
def __init__(self, *,
|
|
29
|
+
host_address: str,
|
|
30
|
+
taskhub: str,
|
|
31
|
+
token_credential: Optional[TokenCredential],
|
|
32
|
+
channel: Optional[grpc.Channel] = None,
|
|
33
|
+
secure_channel: bool = True,
|
|
34
|
+
interceptors: Optional[Sequence[shared.ClientInterceptor]] = None,
|
|
35
|
+
channel_options: Optional[GrpcChannelOptions] = None,
|
|
36
|
+
resiliency_options: Optional[GrpcClientResiliencyOptions] = None,
|
|
37
|
+
default_version: Optional[str] = None,
|
|
38
|
+
payload_store: Optional[PayloadStore] = None,
|
|
39
|
+
log_handler: Optional[logging.Handler] = None,
|
|
40
|
+
log_formatter: Optional[logging.Formatter] = None):
|
|
41
|
+
|
|
42
|
+
if not taskhub:
|
|
43
|
+
raise ValueError("Taskhub value cannot be empty. Please provide a value for your taskhub")
|
|
44
|
+
|
|
45
|
+
resolved_interceptors: list[shared.ClientInterceptor] = (
|
|
46
|
+
list(interceptors) if interceptors is not None else []
|
|
47
|
+
)
|
|
48
|
+
resolved_interceptors.append(DTSDefaultClientInterceptorImpl(token_credential, taskhub))
|
|
49
|
+
|
|
50
|
+
# We pass in None for the metadata so we don't construct an additional interceptor in the parent class
|
|
51
|
+
# Since the parent class doesn't use anything metadata for anything else, we can set it as None
|
|
52
|
+
super().__init__(
|
|
53
|
+
host_address=host_address,
|
|
54
|
+
channel=channel,
|
|
55
|
+
secure_channel=secure_channel,
|
|
56
|
+
metadata=None,
|
|
57
|
+
log_handler=log_handler,
|
|
58
|
+
log_formatter=log_formatter,
|
|
59
|
+
interceptors=resolved_interceptors,
|
|
60
|
+
channel_options=channel_options,
|
|
61
|
+
resiliency_options=resiliency_options,
|
|
62
|
+
default_version=default_version,
|
|
63
|
+
payload_store=payload_store)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# Async client class used for Durable Task Scheduler (DTS)
|
|
67
|
+
class AsyncDurableTaskSchedulerClient(AsyncTaskHubGrpcClient):
|
|
68
|
+
"""An async client implementation for Azure Durable Task Scheduler (DTS).
|
|
69
|
+
|
|
70
|
+
This class extends AsyncTaskHubGrpcClient to provide integration with Azure's
|
|
71
|
+
Durable Task Scheduler service using async gRPC. It handles authentication via
|
|
72
|
+
Azure credentials and configures the necessary gRPC interceptors for DTS
|
|
73
|
+
communication.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
host_address (str): The gRPC endpoint address of the DTS service.
|
|
77
|
+
taskhub (str): The name of the task hub. Cannot be empty.
|
|
78
|
+
token_credential (Optional[TokenCredential]): Azure credential for authentication.
|
|
79
|
+
If None, anonymous authentication will be used.
|
|
80
|
+
secure_channel (bool, optional): Whether to use a secure gRPC channel (TLS).
|
|
81
|
+
Defaults to True.
|
|
82
|
+
resiliency_options (Optional[GrpcClientResiliencyOptions], optional): Client-side
|
|
83
|
+
gRPC resiliency settings forwarded to the base async client.
|
|
84
|
+
default_version (Optional[str], optional): Default version string for orchestrations.
|
|
85
|
+
payload_store (Optional[PayloadStore], optional): A payload store for
|
|
86
|
+
externalizing large payloads. If None, payloads are sent inline.
|
|
87
|
+
log_handler (Optional[logging.Handler], optional): Custom logging handler for client logs.
|
|
88
|
+
log_formatter (Optional[logging.Formatter], optional): Custom log formatter for client logs.
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
ValueError: If taskhub is empty or None.
|
|
92
|
+
|
|
93
|
+
Example:
|
|
94
|
+
>>> from azure.identity.aio import DefaultAzureCredential
|
|
95
|
+
>>> from durabletask.azuremanaged import AsyncDurableTaskSchedulerClient
|
|
96
|
+
>>>
|
|
97
|
+
>>> credential = DefaultAzureCredential()
|
|
98
|
+
>>> async with AsyncDurableTaskSchedulerClient(
|
|
99
|
+
... host_address="my-dts-service.azure.com:443",
|
|
100
|
+
... taskhub="my-task-hub",
|
|
101
|
+
... token_credential=credential
|
|
102
|
+
... ) as client:
|
|
103
|
+
... instance_id = await client.schedule_new_orchestration("my_orchestrator")
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
def __init__(self, *,
|
|
107
|
+
host_address: str,
|
|
108
|
+
taskhub: str,
|
|
109
|
+
token_credential: Optional[AsyncTokenCredential],
|
|
110
|
+
channel: Optional[grpc.aio.Channel] = None,
|
|
111
|
+
secure_channel: bool = True,
|
|
112
|
+
interceptors: Optional[Sequence[shared.AsyncClientInterceptor]] = None,
|
|
113
|
+
channel_options: Optional[GrpcChannelOptions] = None,
|
|
114
|
+
resiliency_options: Optional[GrpcClientResiliencyOptions] = None,
|
|
115
|
+
default_version: Optional[str] = None,
|
|
116
|
+
payload_store: Optional[PayloadStore] = None,
|
|
117
|
+
log_handler: Optional[logging.Handler] = None,
|
|
118
|
+
log_formatter: Optional[logging.Formatter] = None):
|
|
119
|
+
|
|
120
|
+
if not taskhub:
|
|
121
|
+
raise ValueError("Taskhub value cannot be empty. Please provide a value for your taskhub")
|
|
122
|
+
|
|
123
|
+
resolved_interceptors: list[shared.AsyncClientInterceptor] = (
|
|
124
|
+
list(interceptors) if interceptors is not None else []
|
|
125
|
+
)
|
|
126
|
+
resolved_interceptors.append(DTSAsyncDefaultClientInterceptorImpl(token_credential, taskhub))
|
|
127
|
+
|
|
128
|
+
# We pass in None for the metadata so we don't construct an additional interceptor in the parent class
|
|
129
|
+
# Since the parent class doesn't use anything metadata for anything else, we can set it as None
|
|
130
|
+
super().__init__(
|
|
131
|
+
host_address=host_address,
|
|
132
|
+
channel=channel,
|
|
133
|
+
secure_channel=secure_channel,
|
|
134
|
+
metadata=None,
|
|
135
|
+
log_handler=log_handler,
|
|
136
|
+
log_formatter=log_formatter,
|
|
137
|
+
interceptors=resolved_interceptors,
|
|
138
|
+
channel_options=channel_options,
|
|
139
|
+
resiliency_options=resiliency_options,
|
|
140
|
+
default_version=default_version,
|
|
141
|
+
payload_store=payload_store)
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# Copyright (c) Microsoft Corporation.
|
|
2
2
|
# Licensed under the MIT License.
|
|
3
3
|
from datetime import datetime, timedelta, timezone
|
|
4
|
+
from threading import Lock
|
|
4
5
|
from typing import Optional
|
|
5
6
|
|
|
6
7
|
from azure.core.credentials import AccessToken, TokenCredential
|
|
8
|
+
from azure.core.credentials_async import AsyncTokenCredential
|
|
7
9
|
|
|
8
10
|
import durabletask.internal.shared as shared
|
|
9
11
|
|
|
@@ -19,6 +21,7 @@ class AccessTokenManager:
|
|
|
19
21
|
self._logger = shared.get_logger("token_manager")
|
|
20
22
|
|
|
21
23
|
self._credential = token_credential
|
|
24
|
+
self._refresh_lock = Lock()
|
|
22
25
|
|
|
23
26
|
if self._credential is not None:
|
|
24
27
|
self._token = self._credential.get_token(self._scope)
|
|
@@ -29,7 +32,9 @@ class AccessTokenManager:
|
|
|
29
32
|
|
|
30
33
|
def get_access_token(self) -> Optional[AccessToken]:
|
|
31
34
|
if self._token is None or self.is_token_expired():
|
|
32
|
-
self.
|
|
35
|
+
with self._refresh_lock:
|
|
36
|
+
if self._token is None or self.is_token_expired():
|
|
37
|
+
self.refresh_token()
|
|
33
38
|
return self._token
|
|
34
39
|
|
|
35
40
|
# Checks if the token is expired, or if it will expire in the next "refresh_interval_seconds" seconds.
|
|
@@ -47,3 +52,40 @@ class AccessTokenManager:
|
|
|
47
52
|
# Convert UNIX timestamp to timezone-aware datetime
|
|
48
53
|
self.expiry_time = datetime.fromtimestamp(self._token.expires_on, tz=timezone.utc)
|
|
49
54
|
self._logger.debug(f"Token refreshed. Expires at: {self.expiry_time}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class AsyncAccessTokenManager:
|
|
58
|
+
"""Async version of AccessTokenManager that uses AsyncTokenCredential.
|
|
59
|
+
|
|
60
|
+
This avoids blocking the event loop when acquiring or refreshing tokens."""
|
|
61
|
+
|
|
62
|
+
_token: Optional[AccessToken]
|
|
63
|
+
|
|
64
|
+
def __init__(self, token_credential: Optional[AsyncTokenCredential],
|
|
65
|
+
refresh_interval_seconds: int = 600):
|
|
66
|
+
self._scope = "https://durabletask.io/.default"
|
|
67
|
+
self._refresh_interval_seconds = refresh_interval_seconds
|
|
68
|
+
self._logger = shared.get_logger("async_token_manager")
|
|
69
|
+
|
|
70
|
+
self._credential = token_credential
|
|
71
|
+
self._token = None
|
|
72
|
+
self.expiry_time = None
|
|
73
|
+
|
|
74
|
+
async def get_access_token(self) -> Optional[AccessToken]:
|
|
75
|
+
if self._token is None or self.is_token_expired():
|
|
76
|
+
await self.refresh_token()
|
|
77
|
+
return self._token
|
|
78
|
+
|
|
79
|
+
def is_token_expired(self) -> bool:
|
|
80
|
+
if self.expiry_time is None:
|
|
81
|
+
return True
|
|
82
|
+
return datetime.now(timezone.utc) >= (
|
|
83
|
+
self.expiry_time - timedelta(seconds=self._refresh_interval_seconds))
|
|
84
|
+
|
|
85
|
+
async def refresh_token(self):
|
|
86
|
+
if self._credential is not None:
|
|
87
|
+
self._token = await self._credential.get_token(self._scope)
|
|
88
|
+
|
|
89
|
+
# Convert UNIX timestamp to timezone-aware datetime
|
|
90
|
+
self.expiry_time = datetime.fromtimestamp(self._token.expires_on, tz=timezone.utc)
|
|
91
|
+
self._logger.debug(f"Token refreshed. Expires at: {self.expiry_time}")
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
from importlib.metadata import version
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import grpc
|
|
8
|
+
from azure.core.credentials import TokenCredential
|
|
9
|
+
from azure.core.credentials_async import AsyncTokenCredential
|
|
10
|
+
|
|
11
|
+
from durabletask.azuremanaged.internal.access_token_manager import (
|
|
12
|
+
AccessTokenManager,
|
|
13
|
+
AsyncAccessTokenManager,
|
|
14
|
+
)
|
|
15
|
+
from durabletask.internal.grpc_interceptor import (
|
|
16
|
+
DefaultAsyncClientInterceptorImpl,
|
|
17
|
+
DefaultClientInterceptorImpl,
|
|
18
|
+
_AsyncClientCallDetails,
|
|
19
|
+
_ClientCallDetails,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DTSDefaultClientInterceptorImpl (DefaultClientInterceptorImpl):
|
|
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__(
|
|
29
|
+
self,
|
|
30
|
+
token_credential: Optional[TokenCredential],
|
|
31
|
+
taskhub_name: str,
|
|
32
|
+
worker_id: Optional[str] = None):
|
|
33
|
+
try:
|
|
34
|
+
# Get the version of the azuremanaged package
|
|
35
|
+
sdk_version = version('durabletask-azuremanaged')
|
|
36
|
+
except Exception:
|
|
37
|
+
# Fallback if version cannot be determined
|
|
38
|
+
sdk_version = "unknown"
|
|
39
|
+
user_agent = f"durabletask-python/{sdk_version}"
|
|
40
|
+
self._metadata = [
|
|
41
|
+
("taskhub", taskhub_name),
|
|
42
|
+
("x-user-agent", user_agent)] # 'user-agent' is a reserved header; use 'x-user-agent'
|
|
43
|
+
if worker_id is not None:
|
|
44
|
+
self._metadata.append(("workerid", worker_id))
|
|
45
|
+
super().__init__(self._metadata)
|
|
46
|
+
|
|
47
|
+
self._token_manager = None
|
|
48
|
+
if token_credential is not None:
|
|
49
|
+
self._token_credential = token_credential
|
|
50
|
+
self._token_manager = AccessTokenManager(token_credential=self._token_credential)
|
|
51
|
+
access_token = self._token_manager.get_access_token()
|
|
52
|
+
if access_token is not None:
|
|
53
|
+
self._upsert_authorization_header(access_token.token)
|
|
54
|
+
|
|
55
|
+
def _upsert_authorization_header(self, token: str) -> None:
|
|
56
|
+
found = False
|
|
57
|
+
for i, (key, _) in enumerate(self._metadata):
|
|
58
|
+
if key.lower() == "authorization":
|
|
59
|
+
self._metadata[i] = ("authorization", f"Bearer {token}")
|
|
60
|
+
found = True
|
|
61
|
+
break
|
|
62
|
+
if not found:
|
|
63
|
+
self._metadata.append(("authorization", f"Bearer {token}"))
|
|
64
|
+
|
|
65
|
+
def _intercept_call(
|
|
66
|
+
self, client_call_details: _ClientCallDetails) -> grpc.ClientCallDetails:
|
|
67
|
+
"""Internal intercept_call implementation which adds metadata to grpc metadata in the RPC
|
|
68
|
+
call details."""
|
|
69
|
+
# Refresh the auth token if a credential was provided. The call to
|
|
70
|
+
# get_access_token() is generally cheap, checking the expiry time and returning
|
|
71
|
+
# the cached value without a network call when still valid.
|
|
72
|
+
if self._token_manager is not None:
|
|
73
|
+
access_token = self._token_manager.get_access_token()
|
|
74
|
+
if access_token is not None:
|
|
75
|
+
self._upsert_authorization_header(access_token.token)
|
|
76
|
+
|
|
77
|
+
return super()._intercept_call(client_call_details)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class DTSAsyncDefaultClientInterceptorImpl(DefaultAsyncClientInterceptorImpl):
|
|
81
|
+
"""Async version of DTSDefaultClientInterceptorImpl for use with grpc.aio channels.
|
|
82
|
+
|
|
83
|
+
This class implements async gRPC interceptors to add DTS-specific headers
|
|
84
|
+
(task hub name, user agent, and authentication token) to all async calls."""
|
|
85
|
+
|
|
86
|
+
def __init__(self, token_credential: Optional[AsyncTokenCredential], taskhub_name: str):
|
|
87
|
+
try:
|
|
88
|
+
# Get the version of the azuremanaged package
|
|
89
|
+
sdk_version = version('durabletask-azuremanaged')
|
|
90
|
+
except Exception:
|
|
91
|
+
# Fallback if version cannot be determined
|
|
92
|
+
sdk_version = "unknown"
|
|
93
|
+
user_agent = f"durabletask-python/{sdk_version}"
|
|
94
|
+
self._metadata = [
|
|
95
|
+
("taskhub", taskhub_name),
|
|
96
|
+
("x-user-agent", user_agent)]
|
|
97
|
+
super().__init__(self._metadata)
|
|
98
|
+
|
|
99
|
+
# Token acquisition is deferred to the first _intercept_call invocation
|
|
100
|
+
# rather than happening in __init__, because get_token() on an
|
|
101
|
+
# AsyncTokenCredential is async and cannot be awaited in a constructor.
|
|
102
|
+
self._token_manager = None
|
|
103
|
+
if token_credential is not None:
|
|
104
|
+
self._token_credential = token_credential
|
|
105
|
+
self._token_manager = AsyncAccessTokenManager(token_credential=self._token_credential)
|
|
106
|
+
|
|
107
|
+
def _upsert_authorization_header(self, token: str) -> None:
|
|
108
|
+
found = False
|
|
109
|
+
for i, (key, _) in enumerate(self._metadata):
|
|
110
|
+
if key.lower() == "authorization":
|
|
111
|
+
self._metadata[i] = ("authorization", f"Bearer {token}")
|
|
112
|
+
found = True
|
|
113
|
+
break
|
|
114
|
+
if not found:
|
|
115
|
+
self._metadata.append(("authorization", f"Bearer {token}"))
|
|
116
|
+
|
|
117
|
+
async def _intercept_call(
|
|
118
|
+
self, client_call_details: _AsyncClientCallDetails) -> grpc.aio.ClientCallDetails:
|
|
119
|
+
"""Internal intercept_call implementation which adds metadata to grpc metadata in the RPC
|
|
120
|
+
call details."""
|
|
121
|
+
# Refresh the auth token if a credential was provided. The call to
|
|
122
|
+
# get_access_token() is generally cheap, checking the expiry time and returning
|
|
123
|
+
# the cached value without a network call when still valid.
|
|
124
|
+
if self._token_manager is not None:
|
|
125
|
+
access_token = await self._token_manager.get_access_token()
|
|
126
|
+
if access_token is not None:
|
|
127
|
+
self._upsert_authorization_header(access_token.token)
|
|
128
|
+
|
|
129
|
+
return await super()._intercept_call(client_call_details)
|
|
@@ -2,13 +2,23 @@
|
|
|
2
2
|
# Licensed under the MIT License.
|
|
3
3
|
|
|
4
4
|
import logging
|
|
5
|
+
import os
|
|
6
|
+
import socket
|
|
7
|
+
import uuid
|
|
5
8
|
|
|
6
|
-
from typing import Optional
|
|
9
|
+
from typing import Optional, Sequence
|
|
7
10
|
|
|
11
|
+
import grpc
|
|
8
12
|
from azure.core.credentials import TokenCredential
|
|
9
13
|
|
|
10
14
|
from durabletask.azuremanaged.internal.durabletask_grpc_interceptor import \
|
|
11
15
|
DTSDefaultClientInterceptorImpl
|
|
16
|
+
from durabletask.grpc_options import (
|
|
17
|
+
GrpcChannelOptions,
|
|
18
|
+
GrpcWorkerResiliencyOptions,
|
|
19
|
+
)
|
|
20
|
+
import durabletask.internal.shared as shared
|
|
21
|
+
from durabletask.payload.store import PayloadStore
|
|
12
22
|
from durabletask.worker import ConcurrencyOptions, TaskHubGrpcWorker
|
|
13
23
|
|
|
14
24
|
|
|
@@ -27,9 +37,13 @@ class DurableTaskSchedulerWorker(TaskHubGrpcWorker):
|
|
|
27
37
|
If None, anonymous authentication will be used.
|
|
28
38
|
secure_channel (bool, optional): Whether to use a secure gRPC channel (TLS).
|
|
29
39
|
Defaults to True.
|
|
40
|
+
resiliency_options (Optional[GrpcWorkerResiliencyOptions], optional): Worker-side
|
|
41
|
+
gRPC resiliency settings forwarded to the base worker.
|
|
30
42
|
concurrency_options (Optional[ConcurrencyOptions], optional): Configuration
|
|
31
43
|
for controlling worker concurrency limits. If None, default concurrency
|
|
32
44
|
settings will be used.
|
|
45
|
+
payload_store (Optional[PayloadStore], optional): A payload store for
|
|
46
|
+
externalizing large payloads. If None, payloads are sent inline.
|
|
33
47
|
log_handler (Optional[logging.Handler], optional): Custom logging handler for worker logs.
|
|
34
48
|
log_formatter (Optional[logging.Formatter], optional): Custom log formatter for worker logs.
|
|
35
49
|
|
|
@@ -61,23 +75,41 @@ class DurableTaskSchedulerWorker(TaskHubGrpcWorker):
|
|
|
61
75
|
host_address: str,
|
|
62
76
|
taskhub: str,
|
|
63
77
|
token_credential: Optional[TokenCredential],
|
|
78
|
+
channel: Optional[grpc.Channel] = None,
|
|
64
79
|
secure_channel: bool = True,
|
|
80
|
+
interceptors: Optional[Sequence[shared.ClientInterceptor]] = None,
|
|
81
|
+
channel_options: Optional[GrpcChannelOptions] = None,
|
|
82
|
+
resiliency_options: Optional[GrpcWorkerResiliencyOptions] = None,
|
|
65
83
|
concurrency_options: Optional[ConcurrencyOptions] = None,
|
|
84
|
+
payload_store: Optional[PayloadStore] = None,
|
|
66
85
|
log_handler: Optional[logging.Handler] = None,
|
|
67
86
|
log_formatter: Optional[logging.Formatter] = None):
|
|
68
87
|
|
|
69
88
|
if not taskhub:
|
|
70
89
|
raise ValueError("The taskhub value cannot be empty.")
|
|
71
90
|
|
|
72
|
-
|
|
91
|
+
worker_id = f"{socket.gethostname()}:{os.getpid()}:{uuid.uuid4()}"
|
|
92
|
+
resolved_interceptors: list[shared.ClientInterceptor] = (
|
|
93
|
+
list(interceptors) if interceptors is not None else []
|
|
94
|
+
)
|
|
95
|
+
resolved_interceptors.append(
|
|
96
|
+
DTSDefaultClientInterceptorImpl(token_credential, taskhub, worker_id=worker_id)
|
|
97
|
+
)
|
|
73
98
|
|
|
74
99
|
# We pass in None for the metadata so we don't construct an additional interceptor in the parent class
|
|
75
100
|
# Since the parent class doesn't use anything metadata for anything else, we can set it as None
|
|
76
101
|
super().__init__(
|
|
77
102
|
host_address=host_address,
|
|
103
|
+
channel=channel,
|
|
78
104
|
secure_channel=secure_channel,
|
|
79
105
|
metadata=None,
|
|
80
106
|
log_handler=log_handler,
|
|
81
107
|
log_formatter=log_formatter,
|
|
82
|
-
interceptors=
|
|
83
|
-
|
|
108
|
+
interceptors=resolved_interceptors,
|
|
109
|
+
channel_options=channel_options,
|
|
110
|
+
resiliency_options=resiliency_options,
|
|
111
|
+
concurrency_options=concurrency_options,
|
|
112
|
+
# DTS natively supports long timers so chunking is unnecessary
|
|
113
|
+
maximum_timer_interval=None,
|
|
114
|
+
payload_store=payload_store
|
|
115
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: durabletask.azuremanaged
|
|
3
|
-
Version: 0.0.0.
|
|
3
|
+
Version: 0.0.0.dev71
|
|
4
4
|
Summary: Durable Task Python SDK provider implementation for the Azure Durable Task Scheduler
|
|
5
5
|
Project-URL: repository, https://github.com/microsoft/durabletask-python
|
|
6
6
|
Project-URL: changelog, https://github.com/microsoft/durabletask-python/blob/main/CHANGELOG.md
|
|
@@ -12,3 +12,5 @@ Requires-Python: >=3.10
|
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
Requires-Dist: durabletask>=0.0.0dev1
|
|
14
14
|
Requires-Dist: azure-identity>=1.19.0
|
|
15
|
+
Provides-Extra: azure-blob-payloads
|
|
16
|
+
Requires-Dist: durabletask[azure-blob-payloads]>=1.4.0; extra == "azure-blob-payloads"
|
{durabletask_azuremanaged-0.0.0.dev69 → durabletask_azuremanaged-0.0.0.dev71}/pyproject.toml
RENAMED
|
@@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta"
|
|
|
9
9
|
|
|
10
10
|
[project]
|
|
11
11
|
name = "durabletask.azuremanaged"
|
|
12
|
-
version = "0.0.0.
|
|
12
|
+
version = "0.0.0.dev71"
|
|
13
13
|
description = "Durable Task Python SDK provider implementation for the Azure Durable Task Scheduler"
|
|
14
14
|
keywords = [
|
|
15
15
|
"durable",
|
|
@@ -30,6 +30,11 @@ dependencies = [
|
|
|
30
30
|
"azure-identity>=1.19.0"
|
|
31
31
|
]
|
|
32
32
|
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
azure-blob-payloads = [
|
|
35
|
+
"durabletask[azure-blob-payloads]>=1.4.0"
|
|
36
|
+
]
|
|
37
|
+
|
|
33
38
|
[project.urls]
|
|
34
39
|
repository = "https://github.com/microsoft/durabletask-python"
|
|
35
40
|
changelog = "https://github.com/microsoft/durabletask-python/blob/main/CHANGELOG.md"
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
# Copyright (c) Microsoft Corporation.
|
|
2
|
-
# Licensed under the MIT License.
|
|
3
|
-
|
|
4
|
-
import logging
|
|
5
|
-
|
|
6
|
-
from typing import Optional
|
|
7
|
-
|
|
8
|
-
from azure.core.credentials import TokenCredential
|
|
9
|
-
|
|
10
|
-
from durabletask.azuremanaged.internal.durabletask_grpc_interceptor import (
|
|
11
|
-
DTSDefaultClientInterceptorImpl,
|
|
12
|
-
)
|
|
13
|
-
from durabletask.client import TaskHubGrpcClient
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
# Client class used for Durable Task Scheduler (DTS)
|
|
17
|
-
class DurableTaskSchedulerClient(TaskHubGrpcClient):
|
|
18
|
-
def __init__(self, *,
|
|
19
|
-
host_address: str,
|
|
20
|
-
taskhub: str,
|
|
21
|
-
token_credential: Optional[TokenCredential],
|
|
22
|
-
secure_channel: bool = True,
|
|
23
|
-
default_version: Optional[str] = None,
|
|
24
|
-
log_handler: Optional[logging.Handler] = None,
|
|
25
|
-
log_formatter: Optional[logging.Formatter] = None):
|
|
26
|
-
|
|
27
|
-
if not taskhub:
|
|
28
|
-
raise ValueError("Taskhub value cannot be empty. Please provide a value for your taskhub")
|
|
29
|
-
|
|
30
|
-
interceptors = [DTSDefaultClientInterceptorImpl(token_credential, taskhub)]
|
|
31
|
-
|
|
32
|
-
# We pass in None for the metadata so we don't construct an additional interceptor in the parent class
|
|
33
|
-
# Since the parent class doesn't use anything metadata for anything else, we can set it as None
|
|
34
|
-
super().__init__(
|
|
35
|
-
host_address=host_address,
|
|
36
|
-
secure_channel=secure_channel,
|
|
37
|
-
metadata=None,
|
|
38
|
-
log_handler=log_handler,
|
|
39
|
-
log_formatter=log_formatter,
|
|
40
|
-
interceptors=interceptors,
|
|
41
|
-
default_version=default_version)
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
# Copyright (c) Microsoft Corporation.
|
|
2
|
-
# Licensed under the MIT License.
|
|
3
|
-
|
|
4
|
-
from importlib.metadata import version
|
|
5
|
-
from typing import Optional
|
|
6
|
-
|
|
7
|
-
import grpc
|
|
8
|
-
from azure.core.credentials import TokenCredential
|
|
9
|
-
|
|
10
|
-
from durabletask.azuremanaged.internal.access_token_manager import AccessTokenManager
|
|
11
|
-
from durabletask.internal.grpc_interceptor import (
|
|
12
|
-
DefaultClientInterceptorImpl,
|
|
13
|
-
_ClientCallDetails,
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class DTSDefaultClientInterceptorImpl (DefaultClientInterceptorImpl):
|
|
18
|
-
"""The class implements a UnaryUnaryClientInterceptor, UnaryStreamClientInterceptor,
|
|
19
|
-
StreamUnaryClientInterceptor and StreamStreamClientInterceptor from grpc to add an
|
|
20
|
-
interceptor to add additional headers to all calls as needed."""
|
|
21
|
-
|
|
22
|
-
def __init__(self, token_credential: Optional[TokenCredential], taskhub_name: str):
|
|
23
|
-
try:
|
|
24
|
-
# Get the version of the azuremanaged package
|
|
25
|
-
sdk_version = version('durabletask-azuremanaged')
|
|
26
|
-
except Exception:
|
|
27
|
-
# Fallback if version cannot be determined
|
|
28
|
-
sdk_version = "unknown"
|
|
29
|
-
user_agent = f"durabletask-python/{sdk_version}"
|
|
30
|
-
self._metadata = [
|
|
31
|
-
("taskhub", taskhub_name),
|
|
32
|
-
("x-user-agent", user_agent)] # 'user-agent' is a reserved header in grpc, so we use 'x-user-agent' instead
|
|
33
|
-
super().__init__(self._metadata)
|
|
34
|
-
|
|
35
|
-
if token_credential is not None:
|
|
36
|
-
self._token_credential = token_credential
|
|
37
|
-
self._token_manager = AccessTokenManager(token_credential=self._token_credential)
|
|
38
|
-
access_token = self._token_manager.get_access_token()
|
|
39
|
-
if access_token is not None:
|
|
40
|
-
self._metadata.append(("authorization", f"Bearer {access_token.token}"))
|
|
41
|
-
|
|
42
|
-
def _intercept_call(
|
|
43
|
-
self, client_call_details: _ClientCallDetails) -> grpc.ClientCallDetails:
|
|
44
|
-
"""Internal intercept_call implementation which adds metadata to grpc metadata in the RPC
|
|
45
|
-
call details."""
|
|
46
|
-
# Refresh the auth token if it is present and needed
|
|
47
|
-
if self._metadata is not None:
|
|
48
|
-
for i, (key, _) in enumerate(self._metadata):
|
|
49
|
-
if key.lower() == "authorization": # Ensure case-insensitive comparison
|
|
50
|
-
new_token = self._token_manager.get_access_token() # Get the new token
|
|
51
|
-
if new_token is not None:
|
|
52
|
-
self._metadata[i] = ("authorization", f"Bearer {new_token.token}") # Update the token
|
|
53
|
-
|
|
54
|
-
return super()._intercept_call(client_call_details)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|