dex-python-sdk 1.15.0__tar.gz → 1.16.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/PKG-INFO +42 -1
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/README.md +41 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/__init__.py +14 -7
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/_credential_transport.py +103 -4
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/_pull_worker.py +38 -2
- dex_python_sdk-1.16.0/dex/credential.py +547 -0
- dex_python_sdk-1.16.0/dex/dexpb/dex_pb2.py +551 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/dexpb/dex_pb2.pyi +47 -16
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/dexpb/dex_pb2_grpc.py +14 -1
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/flow_options.py +23 -1
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/pyproject.toml +1 -1
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/sdk-rust/Cargo.lock +709 -12
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/sdk-rust/Cargo.toml +19 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/sdk-rust/crates/dex-blob-cache/Cargo.toml +4 -0
- dex_python_sdk-1.16.0/sdk-rust/crates/dex-blob-cache/README.md +9 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/sdk-rust/crates/dex-blob-cache/src/format.rs +15 -3
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/sdk-rust/crates/dex-blob-cache/src/lib.rs +152 -31
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/sdk-rust/crates/dex-blob-cache/src/store.rs +61 -34
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/sdk-rust/crates/dex-blob-cache/tests/blob_cache_integration.rs +83 -6
- dex_python_sdk-1.16.0/sdk-rust/crates/dex-blob-cache-python/Cargo.toml +37 -0
- dex_python_sdk-1.16.0/sdk-rust/crates/dex-blob-cache-python/README.md +5 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/sdk-rust/crates/dex-blob-cache-python/src/lib.rs +2 -0
- dex_python_sdk-1.15.0/dex/credential.py +0 -146
- dex_python_sdk-1.15.0/dex/dexpb/dex_pb2.py +0 -549
- dex_python_sdk-1.15.0/sdk-rust/crates/dex-blob-cache-python/Cargo.toml +0 -17
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/LEGACY_NOTICES.md +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/LICENSE +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/_async_value_hydrator.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/_async_worker_dispatcher.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/_async_worker_service.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/_grpc_errors.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/_invocation_context.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/_native.pyi +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/_server_protocol.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/_utils.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/_value_hydrator.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/_value_mapper.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/_worker_dispatcher.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/_worker_service.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/async_client.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/async_worker.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/attribute.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/attribute_match.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/blob_cache.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/channel.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/client.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/client_options.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/codec.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/condition.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/context.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/dexpb/__init__.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/flow.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/flow_config.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/flow_info.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/flow_result.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/pull_registration.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/py.typed +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/rpc_invoke_options.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/runtime_errors.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/step.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/step_execution.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/stream.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/subflow.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/timer.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/wait.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/wait_options.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/worker.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/dex/worker_options.py +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/sdk-rust/crates/dex-blob-cache/LICENSE +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/sdk-rust/crates/dex-blob-cache/src/config.rs +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/sdk-rust/crates/dex-blob-cache/src/entry.rs +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/sdk-rust/crates/dex-blob-cache/src/error.rs +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/sdk-rust/crates/dex-blob-cache/src/policy.rs +0 -0
- {dex_python_sdk-1.15.0 → dex_python_sdk-1.16.0}/sdk-rust/crates/dex-blob-cache-python/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dex-python-sdk
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.16.0
|
|
4
4
|
Requires-Dist: grpcio>=1.83.0
|
|
5
5
|
Requires-Dist: grpcio-status>=1.83.0
|
|
6
6
|
Requires-Dist: protobuf>=7.35.1
|
|
@@ -25,6 +25,47 @@ checks its mtime and size at most once per second, and reloads it after rotation
|
|
|
25
25
|
Whitespace around the token is removed. An unreadable or empty file fails calls
|
|
26
26
|
until it is restored; the SDK does not send an old token.
|
|
27
27
|
|
|
28
|
+
Use `ApiKeyCredential` when the application holds a long-lived API key and a
|
|
29
|
+
token endpoint issues short-lived access tokens scoped to a server ID. Supply
|
|
30
|
+
the token URL, server ID, and API key explicitly; the server ID is never derived
|
|
31
|
+
from the endpoint host. The endpoint must use HTTPS, except loopback HTTP on
|
|
32
|
+
`localhost`, `127.0.0.0/8`, or `::1` for local development.
|
|
33
|
+
Loopback HTTP bypasses environment proxies so the API key stays on the host.
|
|
34
|
+
HTTPS uses configured proxies with TLS through CONNECT. Token endpoint redirects
|
|
35
|
+
are refused, including redirects that preserve the POST method.
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from dex import ApiKeyCredential, Client, ClientOptions
|
|
39
|
+
|
|
40
|
+
credential = ApiKeyCredential(
|
|
41
|
+
"https://auth.example.com/token", "dex-server", "application-api-key"
|
|
42
|
+
)
|
|
43
|
+
client = Client(registry, cache, ClientOptions(credential=credential))
|
|
44
|
+
try:
|
|
45
|
+
client.health_check()
|
|
46
|
+
finally:
|
|
47
|
+
client.close()
|
|
48
|
+
credential.close()
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The first call exchanges the key for a token. After 80% of its lifetime, a
|
|
52
|
+
single background refresh begins while calls continue using the valid token.
|
|
53
|
+
Callers waiting for an expired token share the exchange. Failed refreshes retry
|
|
54
|
+
no more often than every five seconds while the previous token remains valid.
|
|
55
|
+
Each exchange has a 10-second deadline by default, configurable through
|
|
56
|
+
`exchange_timeout`. It covers connecting, sending, reading, and parsing. A Dex
|
|
57
|
+
`UNAUTHENTICATED` response discards the rejected token
|
|
58
|
+
and retries a unary call once with a fresh token. Pull Worker streams reconnect
|
|
59
|
+
once immediately with a fresh token; a second rejection uses the normal backoff.
|
|
60
|
+
The credential is thread-safe.
|
|
61
|
+
It redacts the API key, issued access tokens, and JWT-shaped strings from
|
|
62
|
+
exchange errors, including endpoint details and transport causes. Client
|
|
63
|
+
calls raise `ApiKeyExchangeError` unchanged; pull Workers log it and retry.
|
|
64
|
+
Closing the credential is optional. After all Clients and Workers sharing it
|
|
65
|
+
are closed, `close()` prevents further token calls and waits at most until the
|
|
66
|
+
current exchange deadline. An uninterruptible network operation may continue
|
|
67
|
+
in its daemon thread after that deadline.
|
|
68
|
+
|
|
28
69
|
```python
|
|
29
70
|
from dex import Client, ClientOptions, FileCredential, Worker, WorkerOptions
|
|
30
71
|
|
|
@@ -9,6 +9,47 @@ checks its mtime and size at most once per second, and reloads it after rotation
|
|
|
9
9
|
Whitespace around the token is removed. An unreadable or empty file fails calls
|
|
10
10
|
until it is restored; the SDK does not send an old token.
|
|
11
11
|
|
|
12
|
+
Use `ApiKeyCredential` when the application holds a long-lived API key and a
|
|
13
|
+
token endpoint issues short-lived access tokens scoped to a server ID. Supply
|
|
14
|
+
the token URL, server ID, and API key explicitly; the server ID is never derived
|
|
15
|
+
from the endpoint host. The endpoint must use HTTPS, except loopback HTTP on
|
|
16
|
+
`localhost`, `127.0.0.0/8`, or `::1` for local development.
|
|
17
|
+
Loopback HTTP bypasses environment proxies so the API key stays on the host.
|
|
18
|
+
HTTPS uses configured proxies with TLS through CONNECT. Token endpoint redirects
|
|
19
|
+
are refused, including redirects that preserve the POST method.
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from dex import ApiKeyCredential, Client, ClientOptions
|
|
23
|
+
|
|
24
|
+
credential = ApiKeyCredential(
|
|
25
|
+
"https://auth.example.com/token", "dex-server", "application-api-key"
|
|
26
|
+
)
|
|
27
|
+
client = Client(registry, cache, ClientOptions(credential=credential))
|
|
28
|
+
try:
|
|
29
|
+
client.health_check()
|
|
30
|
+
finally:
|
|
31
|
+
client.close()
|
|
32
|
+
credential.close()
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The first call exchanges the key for a token. After 80% of its lifetime, a
|
|
36
|
+
single background refresh begins while calls continue using the valid token.
|
|
37
|
+
Callers waiting for an expired token share the exchange. Failed refreshes retry
|
|
38
|
+
no more often than every five seconds while the previous token remains valid.
|
|
39
|
+
Each exchange has a 10-second deadline by default, configurable through
|
|
40
|
+
`exchange_timeout`. It covers connecting, sending, reading, and parsing. A Dex
|
|
41
|
+
`UNAUTHENTICATED` response discards the rejected token
|
|
42
|
+
and retries a unary call once with a fresh token. Pull Worker streams reconnect
|
|
43
|
+
once immediately with a fresh token; a second rejection uses the normal backoff.
|
|
44
|
+
The credential is thread-safe.
|
|
45
|
+
It redacts the API key, issued access tokens, and JWT-shaped strings from
|
|
46
|
+
exchange errors, including endpoint details and transport causes. Client
|
|
47
|
+
calls raise `ApiKeyExchangeError` unchanged; pull Workers log it and retry.
|
|
48
|
+
Closing the credential is optional. After all Clients and Workers sharing it
|
|
49
|
+
are closed, `close()` prevents further token calls and waits at most until the
|
|
50
|
+
current exchange deadline. An uninterruptible network operation may continue
|
|
51
|
+
in its daemon thread after that deadline.
|
|
52
|
+
|
|
12
53
|
```python
|
|
13
54
|
from dex import Client, ClientOptions, FileCredential, Worker, WorkerOptions
|
|
14
55
|
|
|
@@ -24,7 +24,6 @@ from dex.blob_cache import BlobCache, BlobCacheConfig, open_blob_cache
|
|
|
24
24
|
from dex.channel import Channel, ChannelMap, ChannelMapLoad, ChannelMessage
|
|
25
25
|
from dex.client import Client
|
|
26
26
|
from dex.client_options import ClientOptions
|
|
27
|
-
from dex.credential import Credential, FileCredential
|
|
28
27
|
from dex.codec import (
|
|
29
28
|
BOOL,
|
|
30
29
|
BYTES,
|
|
@@ -39,6 +38,12 @@ from dex.codec import (
|
|
|
39
38
|
)
|
|
40
39
|
from dex.condition import ConditionCombination
|
|
41
40
|
from dex.context import AsyncContext, Context, RecoveryErrorInfo
|
|
41
|
+
from dex.credential import (
|
|
42
|
+
ApiKeyCredential,
|
|
43
|
+
ApiKeyExchangeError,
|
|
44
|
+
Credential,
|
|
45
|
+
FileCredential,
|
|
46
|
+
)
|
|
42
47
|
from dex.flow import Flow, PersistenceSchema, Registry, RPCResult, rpc
|
|
43
48
|
from dex.flow_config import ActiveStepSearchMode, FlowConfig
|
|
44
49
|
from dex.flow_info import (
|
|
@@ -62,6 +67,12 @@ from dex.flow_options import (
|
|
|
62
67
|
TimeTravelType,
|
|
63
68
|
)
|
|
64
69
|
from dex.flow_result import FlowResult, StepCompletion
|
|
70
|
+
from dex.pull_registration import (
|
|
71
|
+
PullQueueRegistrationState,
|
|
72
|
+
PullRegistrationError,
|
|
73
|
+
PullRegistrationState,
|
|
74
|
+
)
|
|
75
|
+
from dex.rpc_invoke_options import RPCInvokeOptions
|
|
65
76
|
from dex.runtime_errors import (
|
|
66
77
|
AttributeMapNotLoadedError,
|
|
67
78
|
ChannelMessageNotFoundError,
|
|
@@ -80,7 +91,6 @@ from dex.runtime_errors import (
|
|
|
80
91
|
ValueMappingError,
|
|
81
92
|
WorkerInvocationError,
|
|
82
93
|
)
|
|
83
|
-
from dex.rpc_invoke_options import RPCInvokeOptions
|
|
84
94
|
from dex.step import (
|
|
85
95
|
RetryPolicy,
|
|
86
96
|
Step,
|
|
@@ -113,11 +123,6 @@ from dex.timer import Timer
|
|
|
113
123
|
from dex.wait import Wait
|
|
114
124
|
from dex.wait_options import WaitForAttributeOptions, WaitForStepCompletionOptions
|
|
115
125
|
from dex.worker import Worker
|
|
116
|
-
from dex.pull_registration import (
|
|
117
|
-
PullRegistrationError,
|
|
118
|
-
PullRegistrationState,
|
|
119
|
-
PullQueueRegistrationState,
|
|
120
|
-
)
|
|
121
126
|
from dex.worker_options import PullWorkerOptions, WorkerOptions, WorkerTarget
|
|
122
127
|
|
|
123
128
|
__all__ = [
|
|
@@ -138,6 +143,8 @@ __all__ = [
|
|
|
138
143
|
"AsyncClient",
|
|
139
144
|
"AsyncBufferedTextStream",
|
|
140
145
|
"AsyncWorker",
|
|
146
|
+
"ApiKeyCredential",
|
|
147
|
+
"ApiKeyExchangeError",
|
|
141
148
|
"BlobCache",
|
|
142
149
|
"BlobCacheConfig",
|
|
143
150
|
"BufferedTextStream",
|
|
@@ -16,6 +16,7 @@ import asyncio
|
|
|
16
16
|
import inspect
|
|
17
17
|
import queue
|
|
18
18
|
import threading
|
|
19
|
+
import time
|
|
19
20
|
from collections.abc import Callable
|
|
20
21
|
from concurrent.futures import Future
|
|
21
22
|
from typing import Any, NamedTuple, cast
|
|
@@ -168,6 +169,38 @@ def credential_deadline_error() -> grpc.aio.AioRpcError:
|
|
|
168
169
|
)
|
|
169
170
|
|
|
170
171
|
|
|
172
|
+
class _SyncCredentialDeadlineError(grpc.RpcError):
|
|
173
|
+
def code(self) -> grpc.StatusCode:
|
|
174
|
+
return grpc.StatusCode.DEADLINE_EXCEEDED
|
|
175
|
+
|
|
176
|
+
def details(self) -> str:
|
|
177
|
+
return "credential lookup exceeded RPC deadline"
|
|
178
|
+
|
|
179
|
+
def trailing_metadata(self) -> tuple[Any, ...]:
|
|
180
|
+
return ()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _bounded_credential_metadata(
|
|
184
|
+
credential: Credential, metadata: Any, timeout: float | None
|
|
185
|
+
) -> Any:
|
|
186
|
+
if timeout is not None and timeout <= 0:
|
|
187
|
+
raise _SyncCredentialDeadlineError()
|
|
188
|
+
bounded_token = getattr(credential, "_token_with_timeout", None)
|
|
189
|
+
if not callable(bounded_token) or timeout is None:
|
|
190
|
+
return credential_metadata(credential, metadata)
|
|
191
|
+
try:
|
|
192
|
+
token = bounded_token(timeout)
|
|
193
|
+
except TimeoutError:
|
|
194
|
+
raise _SyncCredentialDeadlineError() from None
|
|
195
|
+
return tuple(metadata or ()) + (("authorization", "Bearer " + token),)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _reject_credential_token(credential: Credential, metadata: Any) -> None:
|
|
199
|
+
reject_token = getattr(credential, "_reject_token", None)
|
|
200
|
+
if callable(reject_token):
|
|
201
|
+
reject_token(metadata[-1][1][7:])
|
|
202
|
+
|
|
203
|
+
|
|
171
204
|
class _SyncCredentialInterceptor(grpc.UnaryUnaryClientInterceptor):
|
|
172
205
|
def __init__(self, credential: Credential) -> None:
|
|
173
206
|
self._credential = credential
|
|
@@ -178,15 +211,49 @@ class _SyncCredentialInterceptor(grpc.UnaryUnaryClientInterceptor):
|
|
|
178
211
|
client_call_details: grpc.ClientCallDetails,
|
|
179
212
|
request: Any,
|
|
180
213
|
) -> Any:
|
|
214
|
+
started_at = time.monotonic()
|
|
215
|
+
timeout = client_call_details.timeout
|
|
216
|
+
metadata = _bounded_credential_metadata(
|
|
217
|
+
self._credential, client_call_details.metadata, timeout
|
|
218
|
+
)
|
|
219
|
+
if timeout is not None:
|
|
220
|
+
timeout -= time.monotonic() - started_at
|
|
221
|
+
if timeout <= 0:
|
|
222
|
+
raise _SyncCredentialDeadlineError()
|
|
181
223
|
details = _SyncCallDetails(
|
|
182
224
|
client_call_details.method,
|
|
183
|
-
|
|
184
|
-
|
|
225
|
+
timeout,
|
|
226
|
+
metadata,
|
|
185
227
|
client_call_details.credentials,
|
|
186
228
|
client_call_details.wait_for_ready,
|
|
187
229
|
client_call_details.compression,
|
|
188
230
|
)
|
|
189
|
-
|
|
231
|
+
try:
|
|
232
|
+
response = continuation(details, request)
|
|
233
|
+
except grpc.RpcError as error:
|
|
234
|
+
if error.code() != grpc.StatusCode.UNAUTHENTICATED:
|
|
235
|
+
raise
|
|
236
|
+
if not callable(getattr(self._credential, "_reject_token", None)):
|
|
237
|
+
raise
|
|
238
|
+
else:
|
|
239
|
+
if response.code() != grpc.StatusCode.UNAUTHENTICATED:
|
|
240
|
+
return response
|
|
241
|
+
if not callable(getattr(self._credential, "_reject_token", None)):
|
|
242
|
+
return response
|
|
243
|
+
_reject_credential_token(self._credential, details.metadata)
|
|
244
|
+
if client_call_details.timeout is not None:
|
|
245
|
+
timeout = client_call_details.timeout - (time.monotonic() - started_at)
|
|
246
|
+
retry_details = details._replace(
|
|
247
|
+
metadata=_bounded_credential_metadata(
|
|
248
|
+
self._credential, client_call_details.metadata, timeout
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
if client_call_details.timeout is not None:
|
|
252
|
+
timeout = client_call_details.timeout - (time.monotonic() - started_at)
|
|
253
|
+
if timeout <= 0:
|
|
254
|
+
raise _SyncCredentialDeadlineError()
|
|
255
|
+
retry_details = retry_details._replace(timeout=timeout)
|
|
256
|
+
return continuation(retry_details, request)
|
|
190
257
|
|
|
191
258
|
|
|
192
259
|
class _AsyncCredentialInterceptor(grpc.aio.UnaryUnaryClientInterceptor):
|
|
@@ -216,7 +283,39 @@ class _AsyncCredentialInterceptor(grpc.aio.UnaryUnaryClientInterceptor):
|
|
|
216
283
|
client_call_details.credentials,
|
|
217
284
|
client_call_details.wait_for_ready,
|
|
218
285
|
)
|
|
219
|
-
|
|
286
|
+
call = await continuation(details, request)
|
|
287
|
+
try:
|
|
288
|
+
await call
|
|
289
|
+
except grpc.aio.AioRpcError as error:
|
|
290
|
+
if error.code() != grpc.StatusCode.UNAUTHENTICATED:
|
|
291
|
+
raise
|
|
292
|
+
if not callable(getattr(self._runner._credential, "_reject_token", None)):
|
|
293
|
+
raise
|
|
294
|
+
_reject_credential_token(self._runner._credential, metadata)
|
|
295
|
+
if client_call_details.timeout is not None:
|
|
296
|
+
remaining_timeout = client_call_details.timeout - (
|
|
297
|
+
loop.time() - started_at
|
|
298
|
+
)
|
|
299
|
+
if remaining_timeout <= 0:
|
|
300
|
+
raise credential_deadline_error() from None
|
|
301
|
+
retry_metadata = await async_credential_metadata(
|
|
302
|
+
self._runner, client_call_details.metadata, remaining_timeout
|
|
303
|
+
)
|
|
304
|
+
if client_call_details.timeout is not None:
|
|
305
|
+
remaining_timeout = client_call_details.timeout - (
|
|
306
|
+
loop.time() - started_at
|
|
307
|
+
)
|
|
308
|
+
if remaining_timeout <= 0:
|
|
309
|
+
raise credential_deadline_error() from None
|
|
310
|
+
retry_details = grpc.aio.ClientCallDetails(
|
|
311
|
+
client_call_details.method,
|
|
312
|
+
remaining_timeout,
|
|
313
|
+
retry_metadata,
|
|
314
|
+
client_call_details.credentials,
|
|
315
|
+
client_call_details.wait_for_ready,
|
|
316
|
+
)
|
|
317
|
+
return await continuation(retry_details, request)
|
|
318
|
+
return call
|
|
220
319
|
|
|
221
320
|
|
|
222
321
|
def sync_channel(server_address: str, credential: Credential | None) -> grpc.Channel:
|
|
@@ -22,7 +22,11 @@ from typing import Any
|
|
|
22
22
|
|
|
23
23
|
import grpc
|
|
24
24
|
|
|
25
|
-
from dex._credential_transport import
|
|
25
|
+
from dex._credential_transport import (
|
|
26
|
+
AsyncCredentialRunner,
|
|
27
|
+
_reject_credential_token,
|
|
28
|
+
async_credential_metadata,
|
|
29
|
+
)
|
|
26
30
|
from dex._grpc_errors import _worker_error_status
|
|
27
31
|
from dex._server_protocol import sdk_version
|
|
28
32
|
from dex.dexpb import dex_pb2 as pb
|
|
@@ -314,6 +318,7 @@ class PullStream:
|
|
|
314
318
|
self._call: Any = None
|
|
315
319
|
self._send_lock = asyncio.Lock()
|
|
316
320
|
self._is_reconnecting = False
|
|
321
|
+
self._stream_metadata: Any = None
|
|
317
322
|
self._deadline_handle: asyncio.TimerHandle | None = None
|
|
318
323
|
self._supports_unknown_name_rejection = False
|
|
319
324
|
|
|
@@ -322,6 +327,7 @@ class PullStream:
|
|
|
322
327
|
|
|
323
328
|
async def run(self) -> None:
|
|
324
329
|
backoff = 0.1
|
|
330
|
+
has_retried_unauthenticated = False
|
|
325
331
|
while not self.runtime.is_stopping:
|
|
326
332
|
self.is_draining = False
|
|
327
333
|
was_registered = False
|
|
@@ -336,12 +342,35 @@ class PullStream:
|
|
|
336
342
|
)
|
|
337
343
|
finally:
|
|
338
344
|
code = error.code() if error is not None else None
|
|
345
|
+
if self._is_registered:
|
|
346
|
+
has_retried_unauthenticated = False
|
|
347
|
+
credential = (
|
|
348
|
+
self.runtime._credential_runner._credential
|
|
349
|
+
if self.runtime._credential_runner is not None
|
|
350
|
+
else None
|
|
351
|
+
)
|
|
352
|
+
can_reject_token = (
|
|
353
|
+
code is grpc.StatusCode.UNAUTHENTICATED
|
|
354
|
+
and self._stream_metadata is not None
|
|
355
|
+
and credential is not None
|
|
356
|
+
and callable(getattr(credential, "_reject_token", None))
|
|
357
|
+
)
|
|
358
|
+
should_retry_unauthenticated = (
|
|
359
|
+
can_reject_token and not has_retried_unauthenticated
|
|
360
|
+
)
|
|
361
|
+
if can_reject_token and credential is not None:
|
|
362
|
+
_reject_credential_token(credential, self._stream_metadata)
|
|
339
363
|
rejection = self.runtime.registration.record_stream_result(
|
|
340
364
|
self.queue,
|
|
341
365
|
self._is_registered,
|
|
342
|
-
|
|
366
|
+
(
|
|
367
|
+
error
|
|
368
|
+
if code in _PERMANENT_CODES and not should_retry_unauthenticated
|
|
369
|
+
else None
|
|
370
|
+
),
|
|
343
371
|
)
|
|
344
372
|
self._is_registered = False
|
|
373
|
+
self._stream_metadata = None
|
|
345
374
|
if (
|
|
346
375
|
rejection is not None
|
|
347
376
|
and self.runtime._stop_on_permanent_stream_rejection
|
|
@@ -357,6 +386,11 @@ class PullStream:
|
|
|
357
386
|
or self._is_retiring
|
|
358
387
|
):
|
|
359
388
|
break
|
|
389
|
+
if should_retry_unauthenticated:
|
|
390
|
+
has_retried_unauthenticated = True
|
|
391
|
+
continue
|
|
392
|
+
if code is grpc.StatusCode.UNAUTHENTICATED:
|
|
393
|
+
has_retried_unauthenticated = False
|
|
360
394
|
if self._is_reconnecting:
|
|
361
395
|
self._is_reconnecting = False
|
|
362
396
|
backoff = 0.1
|
|
@@ -390,6 +424,7 @@ class PullStream:
|
|
|
390
424
|
if self.runtime.is_stopping:
|
|
391
425
|
return False
|
|
392
426
|
self._supports_unknown_name_rejection = False
|
|
427
|
+
self._stream_metadata = None
|
|
393
428
|
if self.runtime._credential_runner is None:
|
|
394
429
|
self._call = self.stub.PollWorkerTask()
|
|
395
430
|
else:
|
|
@@ -398,6 +433,7 @@ class PullStream:
|
|
|
398
433
|
)
|
|
399
434
|
if self.runtime.is_stopping:
|
|
400
435
|
return False
|
|
436
|
+
self._stream_metadata = metadata
|
|
401
437
|
self._call = self.stub.PollWorkerTask(metadata=metadata)
|
|
402
438
|
await self._send(
|
|
403
439
|
pb.PollWorkerTaskRequest(
|