durabletask.azuremanaged 1.3.0.dev20__tar.gz → 1.3.0.dev22__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (17) hide show
  1. {durabletask_azuremanaged-1.3.0.dev20 → durabletask_azuremanaged-1.3.0.dev22}/PKG-INFO +1 -1
  2. durabletask_azuremanaged-1.3.0.dev22/durabletask/azuremanaged/client.py +105 -0
  3. {durabletask_azuremanaged-1.3.0.dev20 → durabletask_azuremanaged-1.3.0.dev22}/durabletask/azuremanaged/internal/access_token_manager.py +38 -0
  4. durabletask_azuremanaged-1.3.0.dev22/durabletask/azuremanaged/internal/durabletask_grpc_interceptor.py +121 -0
  5. {durabletask_azuremanaged-1.3.0.dev20 → durabletask_azuremanaged-1.3.0.dev22}/durabletask.azuremanaged.egg-info/PKG-INFO +1 -1
  6. {durabletask_azuremanaged-1.3.0.dev20 → durabletask_azuremanaged-1.3.0.dev22}/pyproject.toml +1 -1
  7. durabletask_azuremanaged-1.3.0.dev20/durabletask/azuremanaged/client.py +0 -41
  8. durabletask_azuremanaged-1.3.0.dev20/durabletask/azuremanaged/internal/durabletask_grpc_interceptor.py +0 -54
  9. {durabletask_azuremanaged-1.3.0.dev20 → durabletask_azuremanaged-1.3.0.dev22}/durabletask/azuremanaged/__init__.py +0 -0
  10. {durabletask_azuremanaged-1.3.0.dev20 → durabletask_azuremanaged-1.3.0.dev22}/durabletask/azuremanaged/internal/py.typed +0 -0
  11. {durabletask_azuremanaged-1.3.0.dev20 → durabletask_azuremanaged-1.3.0.dev22}/durabletask/azuremanaged/py.typed +0 -0
  12. {durabletask_azuremanaged-1.3.0.dev20 → durabletask_azuremanaged-1.3.0.dev22}/durabletask/azuremanaged/worker.py +0 -0
  13. {durabletask_azuremanaged-1.3.0.dev20 → durabletask_azuremanaged-1.3.0.dev22}/durabletask.azuremanaged.egg-info/SOURCES.txt +0 -0
  14. {durabletask_azuremanaged-1.3.0.dev20 → durabletask_azuremanaged-1.3.0.dev22}/durabletask.azuremanaged.egg-info/dependency_links.txt +0 -0
  15. {durabletask_azuremanaged-1.3.0.dev20 → durabletask_azuremanaged-1.3.0.dev22}/durabletask.azuremanaged.egg-info/requires.txt +0 -0
  16. {durabletask_azuremanaged-1.3.0.dev20 → durabletask_azuremanaged-1.3.0.dev22}/durabletask.azuremanaged.egg-info/top_level.txt +0 -0
  17. {durabletask_azuremanaged-1.3.0.dev20 → durabletask_azuremanaged-1.3.0.dev22}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: durabletask.azuremanaged
3
- Version: 1.3.0.dev20
3
+ Version: 1.3.0.dev22
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
@@ -0,0 +1,105 @@
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
+ from azure.core.credentials_async import AsyncTokenCredential
10
+
11
+ from durabletask.azuremanaged.internal.durabletask_grpc_interceptor import (
12
+ DTSAsyncDefaultClientInterceptorImpl,
13
+ DTSDefaultClientInterceptorImpl,
14
+ )
15
+ from durabletask.client import AsyncTaskHubGrpcClient, TaskHubGrpcClient
16
+
17
+
18
+ # Client class used for Durable Task Scheduler (DTS)
19
+ class DurableTaskSchedulerClient(TaskHubGrpcClient):
20
+ def __init__(self, *,
21
+ host_address: str,
22
+ taskhub: str,
23
+ token_credential: Optional[TokenCredential],
24
+ secure_channel: bool = True,
25
+ default_version: Optional[str] = None,
26
+ log_handler: Optional[logging.Handler] = None,
27
+ log_formatter: Optional[logging.Formatter] = None):
28
+
29
+ if not taskhub:
30
+ raise ValueError("Taskhub value cannot be empty. Please provide a value for your taskhub")
31
+
32
+ interceptors = [DTSDefaultClientInterceptorImpl(token_credential, taskhub)]
33
+
34
+ # We pass in None for the metadata so we don't construct an additional interceptor in the parent class
35
+ # Since the parent class doesn't use anything metadata for anything else, we can set it as None
36
+ super().__init__(
37
+ host_address=host_address,
38
+ secure_channel=secure_channel,
39
+ metadata=None,
40
+ log_handler=log_handler,
41
+ log_formatter=log_formatter,
42
+ interceptors=interceptors,
43
+ default_version=default_version)
44
+
45
+
46
+ # Async client class used for Durable Task Scheduler (DTS)
47
+ class AsyncDurableTaskSchedulerClient(AsyncTaskHubGrpcClient):
48
+ """An async client implementation for Azure Durable Task Scheduler (DTS).
49
+
50
+ This class extends AsyncTaskHubGrpcClient to provide integration with Azure's
51
+ Durable Task Scheduler service using async gRPC. It handles authentication via
52
+ Azure credentials and configures the necessary gRPC interceptors for DTS
53
+ communication.
54
+
55
+ Args:
56
+ host_address (str): The gRPC endpoint address of the DTS service.
57
+ taskhub (str): The name of the task hub. Cannot be empty.
58
+ token_credential (Optional[TokenCredential]): Azure credential for authentication.
59
+ If None, anonymous authentication will be used.
60
+ secure_channel (bool, optional): Whether to use a secure gRPC channel (TLS).
61
+ Defaults to True.
62
+ default_version (Optional[str], optional): Default version string for orchestrations.
63
+ log_handler (Optional[logging.Handler], optional): Custom logging handler for client logs.
64
+ log_formatter (Optional[logging.Formatter], optional): Custom log formatter for client logs.
65
+
66
+ Raises:
67
+ ValueError: If taskhub is empty or None.
68
+
69
+ Example:
70
+ >>> from azure.identity.aio import DefaultAzureCredential
71
+ >>> from durabletask.azuremanaged import AsyncDurableTaskSchedulerClient
72
+ >>>
73
+ >>> credential = DefaultAzureCredential()
74
+ >>> async with AsyncDurableTaskSchedulerClient(
75
+ ... host_address="my-dts-service.azure.com:443",
76
+ ... taskhub="my-task-hub",
77
+ ... token_credential=credential
78
+ ... ) as client:
79
+ ... instance_id = await client.schedule_new_orchestration("my_orchestrator")
80
+ """
81
+
82
+ def __init__(self, *,
83
+ host_address: str,
84
+ taskhub: str,
85
+ token_credential: Optional[AsyncTokenCredential],
86
+ secure_channel: bool = True,
87
+ default_version: Optional[str] = None,
88
+ log_handler: Optional[logging.Handler] = None,
89
+ log_formatter: Optional[logging.Formatter] = None):
90
+
91
+ if not taskhub:
92
+ raise ValueError("Taskhub value cannot be empty. Please provide a value for your taskhub")
93
+
94
+ interceptors = [DTSAsyncDefaultClientInterceptorImpl(token_credential, taskhub)]
95
+
96
+ # We pass in None for the metadata so we don't construct an additional interceptor in the parent class
97
+ # Since the parent class doesn't use anything metadata for anything else, we can set it as None
98
+ super().__init__(
99
+ host_address=host_address,
100
+ secure_channel=secure_channel,
101
+ metadata=None,
102
+ log_handler=log_handler,
103
+ log_formatter=log_formatter,
104
+ interceptors=interceptors,
105
+ default_version=default_version)
@@ -4,6 +4,7 @@ from datetime import datetime, timedelta, timezone
4
4
  from typing import Optional
5
5
 
6
6
  from azure.core.credentials import AccessToken, TokenCredential
7
+ from azure.core.credentials_async import AsyncTokenCredential
7
8
 
8
9
  import durabletask.internal.shared as shared
9
10
 
@@ -47,3 +48,40 @@ class AccessTokenManager:
47
48
  # Convert UNIX timestamp to timezone-aware datetime
48
49
  self.expiry_time = datetime.fromtimestamp(self._token.expires_on, tz=timezone.utc)
49
50
  self._logger.debug(f"Token refreshed. Expires at: {self.expiry_time}")
51
+
52
+
53
+ class AsyncAccessTokenManager:
54
+ """Async version of AccessTokenManager that uses AsyncTokenCredential.
55
+
56
+ This avoids blocking the event loop when acquiring or refreshing tokens."""
57
+
58
+ _token: Optional[AccessToken]
59
+
60
+ def __init__(self, token_credential: Optional[AsyncTokenCredential],
61
+ refresh_interval_seconds: int = 600):
62
+ self._scope = "https://durabletask.io/.default"
63
+ self._refresh_interval_seconds = refresh_interval_seconds
64
+ self._logger = shared.get_logger("async_token_manager")
65
+
66
+ self._credential = token_credential
67
+ self._token = None
68
+ self.expiry_time = None
69
+
70
+ async def get_access_token(self) -> Optional[AccessToken]:
71
+ if self._token is None or self.is_token_expired():
72
+ await self.refresh_token()
73
+ return self._token
74
+
75
+ def is_token_expired(self) -> bool:
76
+ if self.expiry_time is None:
77
+ return True
78
+ return datetime.now(timezone.utc) >= (
79
+ self.expiry_time - timedelta(seconds=self._refresh_interval_seconds))
80
+
81
+ async def refresh_token(self):
82
+ if self._credential is not None:
83
+ self._token = await self._credential.get_token(self._scope)
84
+
85
+ # Convert UNIX timestamp to timezone-aware datetime
86
+ self.expiry_time = datetime.fromtimestamp(self._token.expires_on, tz=timezone.utc)
87
+ self._logger.debug(f"Token refreshed. Expires at: {self.expiry_time}")
@@ -0,0 +1,121 @@
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__(self, token_credential: Optional[TokenCredential], taskhub_name: str):
29
+ try:
30
+ # Get the version of the azuremanaged package
31
+ sdk_version = version('durabletask-azuremanaged')
32
+ except Exception:
33
+ # Fallback if version cannot be determined
34
+ sdk_version = "unknown"
35
+ user_agent = f"durabletask-python/{sdk_version}"
36
+ self._metadata = [
37
+ ("taskhub", taskhub_name),
38
+ ("x-user-agent", user_agent)] # 'user-agent' is a reserved header in grpc, so we use 'x-user-agent' instead
39
+ super().__init__(self._metadata)
40
+
41
+ self._token_manager = None
42
+ if token_credential is not None:
43
+ self._token_credential = token_credential
44
+ self._token_manager = AccessTokenManager(token_credential=self._token_credential)
45
+ access_token = self._token_manager.get_access_token()
46
+ if access_token is not None:
47
+ self._metadata.append(("authorization", f"Bearer {access_token.token}"))
48
+
49
+ def _intercept_call(
50
+ self, client_call_details: _ClientCallDetails) -> grpc.ClientCallDetails:
51
+ """Internal intercept_call implementation which adds metadata to grpc metadata in the RPC
52
+ call details."""
53
+ # Refresh the auth token if a credential was provided. The call to
54
+ # get_access_token() is generally cheap, checking the expiry time and returning
55
+ # the cached value without a network call when still valid.
56
+ if self._token_manager is not None:
57
+ access_token = self._token_manager.get_access_token()
58
+ if access_token is not None:
59
+ # Update the existing authorization header
60
+ found = False
61
+ for i, (key, _) in enumerate(self._metadata):
62
+ if key.lower() == "authorization":
63
+ self._metadata[i] = ("authorization", f"Bearer {access_token.token}")
64
+ found = True
65
+ break
66
+ if not found:
67
+ self._metadata.append(("authorization", f"Bearer {access_token.token}"))
68
+
69
+ return super()._intercept_call(client_call_details)
70
+
71
+
72
+ class DTSAsyncDefaultClientInterceptorImpl(DefaultAsyncClientInterceptorImpl):
73
+ """Async version of DTSDefaultClientInterceptorImpl for use with grpc.aio channels.
74
+
75
+ This class implements async gRPC interceptors to add DTS-specific headers
76
+ (task hub name, user agent, and authentication token) to all async calls."""
77
+
78
+ def __init__(self, token_credential: Optional[AsyncTokenCredential], taskhub_name: str):
79
+ try:
80
+ # Get the version of the azuremanaged package
81
+ sdk_version = version('durabletask-azuremanaged')
82
+ except Exception:
83
+ # Fallback if version cannot be determined
84
+ sdk_version = "unknown"
85
+ user_agent = f"durabletask-python/{sdk_version}"
86
+ self._metadata = [
87
+ ("taskhub", taskhub_name),
88
+ ("x-user-agent", user_agent)]
89
+ super().__init__(self._metadata)
90
+
91
+ # Token acquisition is deferred to the first _intercept_call invocation
92
+ # rather than happening in __init__, because get_token() on an
93
+ # AsyncTokenCredential is async and cannot be awaited in a constructor.
94
+ self._token_manager = None
95
+ if token_credential is not None:
96
+ self._token_credential = token_credential
97
+ self._token_manager = AsyncAccessTokenManager(token_credential=self._token_credential)
98
+
99
+ async def _intercept_call(
100
+ self, client_call_details: _AsyncClientCallDetails) -> grpc.aio.ClientCallDetails:
101
+ """Internal intercept_call implementation which adds metadata to grpc metadata in the RPC
102
+ call details."""
103
+ # Refresh the auth token if a credential was provided. The call to
104
+ # get_access_token() is generally cheap, checking the expiry time and returning
105
+ # the cached value without a network call when still valid.
106
+ if self._token_manager is not None:
107
+ access_token = await self._token_manager.get_access_token()
108
+ if access_token is not None:
109
+ # Update the existing authorization header, or append one if this
110
+ # is the first successful token acquisition (token is lazily
111
+ # fetched on the first call since async constructors aren't possible).
112
+ found = False
113
+ for i, (key, _) in enumerate(self._metadata):
114
+ if key.lower() == "authorization":
115
+ self._metadata[i] = ("authorization", f"Bearer {access_token.token}")
116
+ found = True
117
+ break
118
+ if not found:
119
+ self._metadata.append(("authorization", f"Bearer {access_token.token}"))
120
+
121
+ return await super()._intercept_call(client_call_details)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: durabletask.azuremanaged
3
- Version: 1.3.0.dev20
3
+ Version: 1.3.0.dev22
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
@@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta"
9
9
 
10
10
  [project]
11
11
  name = "durabletask.azuremanaged"
12
- version = "1.3.0.dev20"
12
+ version = "1.3.0.dev22"
13
13
  description = "Durable Task Python SDK provider implementation for the Azure Durable Task Scheduler"
14
14
  keywords = [
15
15
  "durable",
@@ -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)