durabletask.azuremanaged 0.0.0.dev70__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.
Files changed (15) hide show
  1. {durabletask_azuremanaged-0.0.0.dev70 → durabletask_azuremanaged-0.0.0.dev71}/PKG-INFO +2 -2
  2. {durabletask_azuremanaged-0.0.0.dev70 → durabletask_azuremanaged-0.0.0.dev71}/durabletask/azuremanaged/client.py +34 -5
  3. {durabletask_azuremanaged-0.0.0.dev70 → durabletask_azuremanaged-0.0.0.dev71}/durabletask/azuremanaged/internal/access_token_manager.py +5 -1
  4. {durabletask_azuremanaged-0.0.0.dev70 → durabletask_azuremanaged-0.0.0.dev71}/durabletask/azuremanaged/internal/durabletask_grpc_interceptor.py +31 -23
  5. {durabletask_azuremanaged-0.0.0.dev70 → durabletask_azuremanaged-0.0.0.dev71}/durabletask/azuremanaged/worker.py +31 -4
  6. {durabletask_azuremanaged-0.0.0.dev70 → durabletask_azuremanaged-0.0.0.dev71}/durabletask.azuremanaged.egg-info/PKG-INFO +2 -2
  7. {durabletask_azuremanaged-0.0.0.dev70 → durabletask_azuremanaged-0.0.0.dev71}/durabletask.azuremanaged.egg-info/requires.txt +1 -1
  8. {durabletask_azuremanaged-0.0.0.dev70 → durabletask_azuremanaged-0.0.0.dev71}/pyproject.toml +2 -2
  9. {durabletask_azuremanaged-0.0.0.dev70 → durabletask_azuremanaged-0.0.0.dev71}/durabletask/azuremanaged/__init__.py +0 -0
  10. {durabletask_azuremanaged-0.0.0.dev70 → durabletask_azuremanaged-0.0.0.dev71}/durabletask/azuremanaged/internal/py.typed +0 -0
  11. {durabletask_azuremanaged-0.0.0.dev70 → durabletask_azuremanaged-0.0.0.dev71}/durabletask/azuremanaged/py.typed +0 -0
  12. {durabletask_azuremanaged-0.0.0.dev70 → durabletask_azuremanaged-0.0.0.dev71}/durabletask.azuremanaged.egg-info/SOURCES.txt +0 -0
  13. {durabletask_azuremanaged-0.0.0.dev70 → durabletask_azuremanaged-0.0.0.dev71}/durabletask.azuremanaged.egg-info/dependency_links.txt +0 -0
  14. {durabletask_azuremanaged-0.0.0.dev70 → durabletask_azuremanaged-0.0.0.dev71}/durabletask.azuremanaged.egg-info/top_level.txt +0 -0
  15. {durabletask_azuremanaged-0.0.0.dev70 → 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.dev70
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
@@ -13,4 +13,4 @@ Description-Content-Type: text/markdown
13
13
  Requires-Dist: durabletask>=0.0.0dev1
14
14
  Requires-Dist: azure-identity>=1.19.0
15
15
  Provides-Extra: azure-blob-payloads
16
- Requires-Dist: durabletask[azure-blob-payloads]>=1.3.0; extra == "azure-blob-payloads"
16
+ Requires-Dist: durabletask[azure-blob-payloads]>=1.4.0; extra == "azure-blob-payloads"
@@ -3,8 +3,10 @@
3
3
 
4
4
  import logging
5
5
 
6
- from typing import Optional
6
+ from typing import Optional, Sequence
7
7
 
8
+ import grpc
9
+ import grpc.aio
8
10
  from azure.core.credentials import TokenCredential
9
11
  from azure.core.credentials_async import AsyncTokenCredential
10
12
 
@@ -13,6 +15,11 @@ from durabletask.azuremanaged.internal.durabletask_grpc_interceptor import (
13
15
  DTSDefaultClientInterceptorImpl,
14
16
  )
15
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
16
23
  from durabletask.payload.store import PayloadStore
17
24
 
18
25
 
@@ -22,7 +29,11 @@ class DurableTaskSchedulerClient(TaskHubGrpcClient):
22
29
  host_address: str,
23
30
  taskhub: str,
24
31
  token_credential: Optional[TokenCredential],
32
+ channel: Optional[grpc.Channel] = None,
25
33
  secure_channel: bool = True,
34
+ interceptors: Optional[Sequence[shared.ClientInterceptor]] = None,
35
+ channel_options: Optional[GrpcChannelOptions] = None,
36
+ resiliency_options: Optional[GrpcClientResiliencyOptions] = None,
26
37
  default_version: Optional[str] = None,
27
38
  payload_store: Optional[PayloadStore] = None,
28
39
  log_handler: Optional[logging.Handler] = None,
@@ -31,17 +42,23 @@ class DurableTaskSchedulerClient(TaskHubGrpcClient):
31
42
  if not taskhub:
32
43
  raise ValueError("Taskhub value cannot be empty. Please provide a value for your taskhub")
33
44
 
34
- interceptors = [DTSDefaultClientInterceptorImpl(token_credential, taskhub)]
45
+ resolved_interceptors: list[shared.ClientInterceptor] = (
46
+ list(interceptors) if interceptors is not None else []
47
+ )
48
+ resolved_interceptors.append(DTSDefaultClientInterceptorImpl(token_credential, taskhub))
35
49
 
36
50
  # We pass in None for the metadata so we don't construct an additional interceptor in the parent class
37
51
  # Since the parent class doesn't use anything metadata for anything else, we can set it as None
38
52
  super().__init__(
39
53
  host_address=host_address,
54
+ channel=channel,
40
55
  secure_channel=secure_channel,
41
56
  metadata=None,
42
57
  log_handler=log_handler,
43
58
  log_formatter=log_formatter,
44
- interceptors=interceptors,
59
+ interceptors=resolved_interceptors,
60
+ channel_options=channel_options,
61
+ resiliency_options=resiliency_options,
45
62
  default_version=default_version,
46
63
  payload_store=payload_store)
47
64
 
@@ -62,6 +79,8 @@ class AsyncDurableTaskSchedulerClient(AsyncTaskHubGrpcClient):
62
79
  If None, anonymous authentication will be used.
63
80
  secure_channel (bool, optional): Whether to use a secure gRPC channel (TLS).
64
81
  Defaults to True.
82
+ resiliency_options (Optional[GrpcClientResiliencyOptions], optional): Client-side
83
+ gRPC resiliency settings forwarded to the base async client.
65
84
  default_version (Optional[str], optional): Default version string for orchestrations.
66
85
  payload_store (Optional[PayloadStore], optional): A payload store for
67
86
  externalizing large payloads. If None, payloads are sent inline.
@@ -88,7 +107,11 @@ class AsyncDurableTaskSchedulerClient(AsyncTaskHubGrpcClient):
88
107
  host_address: str,
89
108
  taskhub: str,
90
109
  token_credential: Optional[AsyncTokenCredential],
110
+ channel: Optional[grpc.aio.Channel] = None,
91
111
  secure_channel: bool = True,
112
+ interceptors: Optional[Sequence[shared.AsyncClientInterceptor]] = None,
113
+ channel_options: Optional[GrpcChannelOptions] = None,
114
+ resiliency_options: Optional[GrpcClientResiliencyOptions] = None,
92
115
  default_version: Optional[str] = None,
93
116
  payload_store: Optional[PayloadStore] = None,
94
117
  log_handler: Optional[logging.Handler] = None,
@@ -97,16 +120,22 @@ class AsyncDurableTaskSchedulerClient(AsyncTaskHubGrpcClient):
97
120
  if not taskhub:
98
121
  raise ValueError("Taskhub value cannot be empty. Please provide a value for your taskhub")
99
122
 
100
- interceptors = [DTSAsyncDefaultClientInterceptorImpl(token_credential, taskhub)]
123
+ resolved_interceptors: list[shared.AsyncClientInterceptor] = (
124
+ list(interceptors) if interceptors is not None else []
125
+ )
126
+ resolved_interceptors.append(DTSAsyncDefaultClientInterceptorImpl(token_credential, taskhub))
101
127
 
102
128
  # We pass in None for the metadata so we don't construct an additional interceptor in the parent class
103
129
  # Since the parent class doesn't use anything metadata for anything else, we can set it as None
104
130
  super().__init__(
105
131
  host_address=host_address,
132
+ channel=channel,
106
133
  secure_channel=secure_channel,
107
134
  metadata=None,
108
135
  log_handler=log_handler,
109
136
  log_formatter=log_formatter,
110
- interceptors=interceptors,
137
+ interceptors=resolved_interceptors,
138
+ channel_options=channel_options,
139
+ resiliency_options=resiliency_options,
111
140
  default_version=default_version,
112
141
  payload_store=payload_store)
@@ -1,6 +1,7 @@
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
@@ -20,6 +21,7 @@ class AccessTokenManager:
20
21
  self._logger = shared.get_logger("token_manager")
21
22
 
22
23
  self._credential = token_credential
24
+ self._refresh_lock = Lock()
23
25
 
24
26
  if self._credential is not None:
25
27
  self._token = self._credential.get_token(self._scope)
@@ -30,7 +32,9 @@ class AccessTokenManager:
30
32
 
31
33
  def get_access_token(self) -> Optional[AccessToken]:
32
34
  if self._token is None or self.is_token_expired():
33
- self.refresh_token()
35
+ with self._refresh_lock:
36
+ if self._token is None or self.is_token_expired():
37
+ self.refresh_token()
34
38
  return self._token
35
39
 
36
40
  # Checks if the token is expired, or if it will expire in the next "refresh_interval_seconds" seconds.
@@ -25,7 +25,11 @@ class DTSDefaultClientInterceptorImpl (DefaultClientInterceptorImpl):
25
25
  StreamUnaryClientInterceptor and StreamStreamClientInterceptor from grpc to add an
26
26
  interceptor to add additional headers to all calls as needed."""
27
27
 
28
- def __init__(self, token_credential: Optional[TokenCredential], taskhub_name: str):
28
+ def __init__(
29
+ self,
30
+ token_credential: Optional[TokenCredential],
31
+ taskhub_name: str,
32
+ worker_id: Optional[str] = None):
29
33
  try:
30
34
  # Get the version of the azuremanaged package
31
35
  sdk_version = version('durabletask-azuremanaged')
@@ -35,7 +39,9 @@ class DTSDefaultClientInterceptorImpl (DefaultClientInterceptorImpl):
35
39
  user_agent = f"durabletask-python/{sdk_version}"
36
40
  self._metadata = [
37
41
  ("taskhub", taskhub_name),
38
- ("x-user-agent", user_agent)] # 'user-agent' is a reserved header in grpc, so we use 'x-user-agent' instead
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))
39
45
  super().__init__(self._metadata)
40
46
 
41
47
  self._token_manager = None
@@ -44,7 +50,17 @@ class DTSDefaultClientInterceptorImpl (DefaultClientInterceptorImpl):
44
50
  self._token_manager = AccessTokenManager(token_credential=self._token_credential)
45
51
  access_token = self._token_manager.get_access_token()
46
52
  if access_token is not None:
47
- self._metadata.append(("authorization", f"Bearer {access_token.token}"))
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}"))
48
64
 
49
65
  def _intercept_call(
50
66
  self, client_call_details: _ClientCallDetails) -> grpc.ClientCallDetails:
@@ -56,15 +72,7 @@ class DTSDefaultClientInterceptorImpl (DefaultClientInterceptorImpl):
56
72
  if self._token_manager is not None:
57
73
  access_token = self._token_manager.get_access_token()
58
74
  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}"))
75
+ self._upsert_authorization_header(access_token.token)
68
76
 
69
77
  return super()._intercept_call(client_call_details)
70
78
 
@@ -96,6 +104,16 @@ class DTSAsyncDefaultClientInterceptorImpl(DefaultAsyncClientInterceptorImpl):
96
104
  self._token_credential = token_credential
97
105
  self._token_manager = AsyncAccessTokenManager(token_credential=self._token_credential)
98
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
+
99
117
  async def _intercept_call(
100
118
  self, client_call_details: _AsyncClientCallDetails) -> grpc.aio.ClientCallDetails:
101
119
  """Internal intercept_call implementation which adds metadata to grpc metadata in the RPC
@@ -106,16 +124,6 @@ class DTSAsyncDefaultClientInterceptorImpl(DefaultAsyncClientInterceptorImpl):
106
124
  if self._token_manager is not None:
107
125
  access_token = await self._token_manager.get_access_token()
108
126
  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}"))
127
+ self._upsert_authorization_header(access_token.token)
120
128
 
121
129
  return await super()._intercept_call(client_call_details)
@@ -2,13 +2,22 @@
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
12
21
  from durabletask.payload.store import PayloadStore
13
22
  from durabletask.worker import ConcurrencyOptions, TaskHubGrpcWorker
14
23
 
@@ -28,6 +37,8 @@ class DurableTaskSchedulerWorker(TaskHubGrpcWorker):
28
37
  If None, anonymous authentication will be used.
29
38
  secure_channel (bool, optional): Whether to use a secure gRPC channel (TLS).
30
39
  Defaults to True.
40
+ resiliency_options (Optional[GrpcWorkerResiliencyOptions], optional): Worker-side
41
+ gRPC resiliency settings forwarded to the base worker.
31
42
  concurrency_options (Optional[ConcurrencyOptions], optional): Configuration
32
43
  for controlling worker concurrency limits. If None, default concurrency
33
44
  settings will be used.
@@ -64,7 +75,11 @@ class DurableTaskSchedulerWorker(TaskHubGrpcWorker):
64
75
  host_address: str,
65
76
  taskhub: str,
66
77
  token_credential: Optional[TokenCredential],
78
+ channel: Optional[grpc.Channel] = None,
67
79
  secure_channel: bool = True,
80
+ interceptors: Optional[Sequence[shared.ClientInterceptor]] = None,
81
+ channel_options: Optional[GrpcChannelOptions] = None,
82
+ resiliency_options: Optional[GrpcWorkerResiliencyOptions] = None,
68
83
  concurrency_options: Optional[ConcurrencyOptions] = None,
69
84
  payload_store: Optional[PayloadStore] = None,
70
85
  log_handler: Optional[logging.Handler] = None,
@@ -73,16 +88,28 @@ class DurableTaskSchedulerWorker(TaskHubGrpcWorker):
73
88
  if not taskhub:
74
89
  raise ValueError("The taskhub value cannot be empty.")
75
90
 
76
- interceptors = [DTSDefaultClientInterceptorImpl(token_credential, taskhub)]
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
+ )
77
98
 
78
99
  # We pass in None for the metadata so we don't construct an additional interceptor in the parent class
79
100
  # Since the parent class doesn't use anything metadata for anything else, we can set it as None
80
101
  super().__init__(
81
102
  host_address=host_address,
103
+ channel=channel,
82
104
  secure_channel=secure_channel,
83
105
  metadata=None,
84
106
  log_handler=log_handler,
85
107
  log_formatter=log_formatter,
86
- interceptors=interceptors,
108
+ interceptors=resolved_interceptors,
109
+ channel_options=channel_options,
110
+ resiliency_options=resiliency_options,
87
111
  concurrency_options=concurrency_options,
88
- payload_store=payload_store)
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.dev70
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
@@ -13,4 +13,4 @@ Description-Content-Type: text/markdown
13
13
  Requires-Dist: durabletask>=0.0.0dev1
14
14
  Requires-Dist: azure-identity>=1.19.0
15
15
  Provides-Extra: azure-blob-payloads
16
- Requires-Dist: durabletask[azure-blob-payloads]>=1.3.0; extra == "azure-blob-payloads"
16
+ Requires-Dist: durabletask[azure-blob-payloads]>=1.4.0; extra == "azure-blob-payloads"
@@ -2,4 +2,4 @@ durabletask>=0.0.0dev1
2
2
  azure-identity>=1.19.0
3
3
 
4
4
  [azure-blob-payloads]
5
- durabletask[azure-blob-payloads]>=1.3.0
5
+ durabletask[azure-blob-payloads]>=1.4.0
@@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta"
9
9
 
10
10
  [project]
11
11
  name = "durabletask.azuremanaged"
12
- version = "0.0.0.dev70"
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",
@@ -32,7 +32,7 @@ dependencies = [
32
32
 
33
33
  [project.optional-dependencies]
34
34
  azure-blob-payloads = [
35
- "durabletask[azure-blob-payloads]>=1.3.0"
35
+ "durabletask[azure-blob-payloads]>=1.4.0"
36
36
  ]
37
37
 
38
38
  [project.urls]