earthscope-sdk 0.2.0__py3-none-any.whl → 1.0.0b0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- earthscope_sdk/__init__.py +5 -1
- earthscope_sdk/auth/auth_flow.py +224 -347
- earthscope_sdk/auth/client_credentials_flow.py +46 -156
- earthscope_sdk/auth/device_code_flow.py +154 -207
- earthscope_sdk/auth/error.py +46 -0
- earthscope_sdk/client/__init__.py +3 -0
- earthscope_sdk/client/_client.py +35 -0
- earthscope_sdk/client/user/_base.py +39 -0
- earthscope_sdk/client/user/_service.py +94 -0
- earthscope_sdk/client/user/models.py +53 -0
- earthscope_sdk/common/__init__.py +0 -0
- earthscope_sdk/common/_sync_runner.py +141 -0
- earthscope_sdk/common/client.py +99 -0
- earthscope_sdk/common/context.py +174 -0
- earthscope_sdk/common/service.py +54 -0
- earthscope_sdk/config/__init__.py +0 -0
- earthscope_sdk/config/_compat.py +148 -0
- earthscope_sdk/config/_util.py +48 -0
- earthscope_sdk/config/error.py +4 -0
- earthscope_sdk/config/models.py +208 -0
- earthscope_sdk/config/settings.py +284 -0
- {earthscope_sdk-0.2.0.dist-info → earthscope_sdk-1.0.0b0.dist-info}/METADATA +144 -123
- earthscope_sdk-1.0.0b0.dist-info/RECORD +28 -0
- {earthscope_sdk-0.2.0.dist-info → earthscope_sdk-1.0.0b0.dist-info}/WHEEL +1 -1
- earthscope_sdk/user/user.py +0 -32
- earthscope_sdk-0.2.0.dist-info/RECORD +0 -12
- /earthscope_sdk/{user → client/user}/__init__.py +0 -0
- {earthscope_sdk-0.2.0.dist-info → earthscope_sdk-1.0.0b0.dist-info}/LICENSE +0 -0
- {earthscope_sdk-0.2.0.dist-info → earthscope_sdk-1.0.0b0.dist-info}/top_level.txt +0 -0
earthscope_sdk/__init__.py
CHANGED
earthscope_sdk/auth/auth_flow.py
CHANGED
@@ -1,441 +1,318 @@
|
|
1
1
|
import datetime as dt
|
2
|
-
import json
|
3
2
|
import logging
|
3
|
+
from contextlib import suppress
|
4
|
+
from typing import Optional
|
4
5
|
|
5
|
-
|
6
|
-
from
|
7
|
-
from typing import Dict, Optional, Union
|
8
|
-
import jwt
|
6
|
+
import httpx
|
7
|
+
from pydantic import ValidationError
|
9
8
|
|
10
|
-
import
|
9
|
+
from earthscope_sdk.auth.error import (
|
10
|
+
InvalidRefreshTokenError,
|
11
|
+
NoAccessTokenError,
|
12
|
+
NoRefreshTokenError,
|
13
|
+
NoTokensError,
|
14
|
+
)
|
15
|
+
from earthscope_sdk.common.context import SdkContext
|
16
|
+
from earthscope_sdk.config.models import AccessTokenBody, Tokens
|
11
17
|
|
12
18
|
logger = logging.getLogger(__name__)
|
13
19
|
|
14
20
|
|
15
|
-
class
|
16
|
-
"""Generic authentication flow error"""
|
17
|
-
|
18
|
-
|
19
|
-
class UnauthorizedError(AuthFlowError):
|
20
|
-
pass
|
21
|
-
|
22
|
-
|
23
|
-
class NoTokensError(AuthFlowError):
|
24
|
-
pass
|
25
|
-
|
26
|
-
|
27
|
-
class NoIdTokenError(NoTokensError):
|
28
|
-
pass
|
29
|
-
|
30
|
-
|
31
|
-
class NoRefreshTokenError(NoTokensError):
|
32
|
-
pass
|
33
|
-
|
34
|
-
|
35
|
-
class InvalidRefreshTokenError(AuthFlowError):
|
36
|
-
pass
|
37
|
-
|
38
|
-
|
39
|
-
@dataclass
|
40
|
-
class TokensResponse:
|
41
|
-
access_token: str
|
42
|
-
refresh_token: Optional[str]
|
43
|
-
id_token: Optional[str]
|
44
|
-
token_type: str
|
45
|
-
expires_in: int
|
46
|
-
scope: Optional[str]
|
47
|
-
|
48
|
-
|
49
|
-
@dataclass
|
50
|
-
class UnvalidatedTokens:
|
51
|
-
access_token: str
|
52
|
-
id_token: Optional[str]
|
53
|
-
refresh_token: Optional[str]
|
54
|
-
scope: str
|
55
|
-
|
56
|
-
@classmethod
|
57
|
-
def from_dict(cls, o: Dict):
|
58
|
-
return cls(
|
59
|
-
access_token=o.get("access_token"),
|
60
|
-
id_token=o.get("id_token"),
|
61
|
-
refresh_token=o.get("refresh_token"),
|
62
|
-
scope=o.get("scope"),
|
63
|
-
)
|
64
|
-
|
65
|
-
|
66
|
-
@dataclass
|
67
|
-
class ValidTokens(UnvalidatedTokens):
|
68
|
-
expires_at: int
|
69
|
-
issued_at: int
|
70
|
-
|
71
|
-
@classmethod
|
72
|
-
def from_unvalidated(cls, creds: UnvalidatedTokens, body: Dict):
|
73
|
-
return cls(
|
74
|
-
access_token=creds.access_token,
|
75
|
-
id_token=creds.id_token,
|
76
|
-
refresh_token=creds.refresh_token,
|
77
|
-
scope=creds.scope,
|
78
|
-
expires_at=body.get("exp"),
|
79
|
-
issued_at=body.get("iat"),
|
80
|
-
)
|
81
|
-
|
82
|
-
|
83
|
-
class AuthFlow(ABC):
|
21
|
+
class AuthFlow(httpx.Auth):
|
84
22
|
"""
|
85
|
-
|
86
|
-
|
87
|
-
Attributes
|
88
|
-
__________
|
89
|
-
domain : str
|
90
|
-
Auth0 tenant domain URL or custom domain (default is 'login.earthscope.org')
|
91
|
-
audience : str
|
92
|
-
Auth0 API Identifier (default is 'https://account.earthscope.org')
|
93
|
-
client_id : str
|
94
|
-
Identification value of Auth0 Application (default is 'b9DtAFBd6QvMg761vI3YhYquNZbJX5G0')
|
95
|
-
scope : str
|
96
|
-
The specific actions Auth0 applications can be allowed to do or information that they can request on a user’s behalf.
|
97
|
-
_tokens : str, optional
|
98
|
-
Access or ID Token
|
99
|
-
_access_token_body : dict, optional
|
100
|
-
Body of the decrypted access token
|
101
|
-
_id_token_body : dict, optional
|
102
|
-
Body of the decrypted id token
|
103
|
-
|
104
|
-
Methods
|
105
|
-
_______
|
106
|
-
load_tokens
|
107
|
-
Load the token (abstract method)
|
108
|
-
save_tokens(creds)
|
109
|
-
Save the token (abstract method)
|
110
|
-
refresh(scope=None, revoke=False)
|
111
|
-
Refresh the access token or revoke the refresh token
|
112
|
-
validate_and_save_tokens(unvalidated_creds)
|
113
|
-
Validate and save the token
|
114
|
-
validate_tokens(unvalidated_creds)
|
115
|
-
Validate the token
|
116
|
-
get_access_token_refresh_if_necessary(no_auto_refresh=False, auto_refresh_threshold=3600)
|
117
|
-
Retrieve the access token. Automatically refreshes the token when necessary.
|
118
|
-
|
23
|
+
Generic oauth2 flow class for handling retrieving, validating, saving, and refreshing access tokens.
|
119
24
|
"""
|
25
|
+
|
120
26
|
@property
|
121
27
|
def access_token(self):
|
122
28
|
"""
|
123
|
-
|
124
|
-
|
125
|
-
Returns
|
126
|
-
-------
|
127
|
-
access token: str
|
128
|
-
"""
|
129
|
-
return self.tokens.access_token
|
29
|
+
Access token
|
130
30
|
|
131
|
-
|
132
|
-
|
31
|
+
Raises:
|
32
|
+
NoTokensError: no tokens (at all) are present
|
33
|
+
NoAccessTokenError: no access token is present
|
133
34
|
"""
|
134
|
-
|
35
|
+
if at := self.tokens.access_token:
|
36
|
+
return at.get_secret_value()
|
135
37
|
|
136
|
-
|
137
|
-
______
|
138
|
-
NoTokensError
|
139
|
-
If there is no token
|
140
|
-
|
141
|
-
Returns
|
142
|
-
-------
|
143
|
-
access_token_body: dict
|
144
|
-
"""
|
145
|
-
if self._access_token_body is None:
|
146
|
-
raise NoTokensError
|
147
|
-
return self._access_token_body
|
38
|
+
raise NoAccessTokenError("No access token was found. Please re-authenticate.")
|
148
39
|
|
149
40
|
@property
|
150
|
-
def
|
41
|
+
def access_token_body(self):
|
151
42
|
"""
|
152
|
-
|
43
|
+
Access token body
|
153
44
|
|
154
|
-
|
155
|
-
|
156
|
-
NoTokensError
|
157
|
-
If no token to retrieve
|
158
|
-
|
159
|
-
Returns
|
160
|
-
-------
|
161
|
-
tokens
|
45
|
+
Raises:
|
46
|
+
NoAccessTokenError: no access token is present
|
162
47
|
"""
|
163
|
-
if
|
164
|
-
|
48
|
+
if body := self._access_token_body:
|
49
|
+
return body
|
165
50
|
|
166
|
-
|
51
|
+
raise NoAccessTokenError("No access token was found. Please re-authenticate.")
|
167
52
|
|
168
53
|
@property
|
169
54
|
def expires_at(self):
|
170
55
|
"""
|
171
|
-
|
56
|
+
Time of access token expiration
|
172
57
|
|
173
|
-
|
174
|
-
|
175
|
-
datetime
|
58
|
+
Raises:
|
59
|
+
NoAccessTokenError: no access token is present
|
176
60
|
"""
|
177
|
-
return
|
61
|
+
return self.access_token_body.expires_at
|
178
62
|
|
179
63
|
@property
|
180
|
-
def
|
64
|
+
def has_refresh_token(self):
|
181
65
|
"""
|
182
|
-
|
183
|
-
|
184
|
-
Raises
|
185
|
-
------
|
186
|
-
NoIDTokenError
|
187
|
-
If there is no id token to be retrieved
|
188
|
-
|
189
|
-
Returns
|
190
|
-
-------
|
191
|
-
id token: srt
|
66
|
+
Whether or not we have a refresh token
|
192
67
|
"""
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
return idt
|
68
|
+
try:
|
69
|
+
return self.refresh_token is not None
|
70
|
+
except NoTokensError:
|
71
|
+
return False
|
198
72
|
|
199
73
|
@property
|
200
|
-
def
|
74
|
+
def issued_at(self):
|
201
75
|
"""
|
202
|
-
|
203
|
-
|
204
|
-
Raises
|
205
|
-
______
|
206
|
-
NoIdTokenError
|
207
|
-
If there is no id token
|
76
|
+
Time of access token creation
|
208
77
|
|
209
|
-
|
210
|
-
|
211
|
-
id_token_body: dict
|
78
|
+
Raises:
|
79
|
+
NoAccessTokenError: no access token is present
|
212
80
|
"""
|
213
|
-
|
214
|
-
raise NoIdTokenError
|
215
|
-
return self._id_token_body
|
81
|
+
return self.access_token_body.issued_at
|
216
82
|
|
217
83
|
@property
|
218
|
-
def
|
84
|
+
def refresh_token(self):
|
219
85
|
"""
|
220
|
-
|
86
|
+
Refresh token
|
221
87
|
|
222
|
-
|
223
|
-
|
224
|
-
|
88
|
+
Raises:
|
89
|
+
NoTokensError: no tokens (at all) are present
|
90
|
+
NoRefreshTokenError: no refresh token is present
|
225
91
|
"""
|
226
|
-
|
92
|
+
if rt := self.tokens.refresh_token:
|
93
|
+
return rt.get_secret_value()
|
94
|
+
|
95
|
+
raise NoRefreshTokenError("No refresh token was found. Please re-authenticate.")
|
227
96
|
|
228
97
|
@property
|
229
|
-
def
|
98
|
+
def scope(self):
|
230
99
|
"""
|
231
|
-
|
232
|
-
|
233
|
-
Raises
|
234
|
-
------
|
235
|
-
NoRefreshTokenError
|
236
|
-
If there is no refresh token to be retrieved
|
100
|
+
Access token scope
|
237
101
|
|
238
|
-
|
239
|
-
|
240
|
-
refresh token: srt
|
102
|
+
Raises:
|
103
|
+
NoAccessTokenError: no access token is present
|
241
104
|
"""
|
242
|
-
|
243
|
-
if not rt:
|
244
|
-
raise NoRefreshTokenError
|
245
|
-
|
246
|
-
return rt
|
105
|
+
return set(self.access_token_body.scope.split())
|
247
106
|
|
248
107
|
@property
|
249
|
-
def
|
108
|
+
def tokens(self):
|
250
109
|
"""
|
251
|
-
|
110
|
+
Oauth2 tokens managed by this auth flow
|
252
111
|
|
253
|
-
|
254
|
-
|
255
|
-
boolean
|
112
|
+
Raises:
|
113
|
+
NoTokensError: no tokens (at all) are present
|
256
114
|
"""
|
257
|
-
|
258
|
-
return
|
259
|
-
|
260
|
-
|
115
|
+
if tokens := self._tokens:
|
116
|
+
return tokens
|
117
|
+
|
118
|
+
raise NoTokensError("No tokens were found. Please re-authenticate.")
|
261
119
|
|
262
120
|
@property
|
263
121
|
def ttl(self):
|
264
122
|
"""
|
265
|
-
|
123
|
+
Access token time-to-live (ttl) before expiration
|
266
124
|
|
267
|
-
|
268
|
-
|
269
|
-
datetime
|
125
|
+
Raises:
|
126
|
+
NoAccessTokenError: no access token is present
|
270
127
|
"""
|
271
|
-
return self.
|
128
|
+
return self.access_token_body.ttl
|
129
|
+
|
130
|
+
def __init__(self, ctx: SdkContext) -> None:
|
131
|
+
self._ctx = ctx
|
132
|
+
self._settings = ctx.settings.oauth2
|
133
|
+
|
134
|
+
# Local state
|
135
|
+
self._access_token_body: Optional[AccessTokenBody] = None
|
136
|
+
self._tokens: Optional[Tokens] = None
|
272
137
|
|
273
|
-
|
274
|
-
|
275
|
-
self.
|
276
|
-
self.
|
277
|
-
self.
|
138
|
+
# httpx.Auth objects can be used in either sync or async clients so we
|
139
|
+
# facilitate both from the same class
|
140
|
+
self.refresh = ctx.syncify(self.async_refresh)
|
141
|
+
self.refresh_if_necessary = ctx.syncify(self.async_refresh_if_necessary)
|
142
|
+
self.revoke_refresh_token = ctx.syncify(self.async_revoke_refresh_token)
|
278
143
|
|
279
|
-
|
280
|
-
|
281
|
-
|
144
|
+
# Initialize with provided tokens
|
145
|
+
with suppress(ValidationError):
|
146
|
+
self._validate_tokens(self._settings)
|
282
147
|
|
283
|
-
|
284
|
-
def load_tokens(self):
|
148
|
+
async def async_refresh(self, scope: Optional[str] = None):
|
285
149
|
"""
|
286
|
-
|
150
|
+
Refresh the access token
|
151
|
+
|
152
|
+
Args:
|
153
|
+
scope: the specific oauth2 scopes to request
|
154
|
+
|
155
|
+
Raises:
|
156
|
+
NoTokensError: no tokens (at all) are present
|
157
|
+
NoRefreshTokenError: no refresh token is present
|
158
|
+
InvalidRefreshTokenError: the token refresh failed
|
159
|
+
"""
|
160
|
+
refresh_token = self.refresh_token
|
161
|
+
scope = scope or self._settings.scope
|
162
|
+
|
163
|
+
r = await self._ctx.httpx_client.post(
|
164
|
+
f"{self._settings.domain}oauth/token",
|
165
|
+
auth=None, # override client default
|
166
|
+
headers={"content-type": "application/x-www-form-urlencoded"},
|
167
|
+
data={
|
168
|
+
"grant_type": "refresh_token",
|
169
|
+
"client_id": self._settings.client_id,
|
170
|
+
"refresh_token": refresh_token,
|
171
|
+
"scopes": scope,
|
172
|
+
},
|
173
|
+
)
|
174
|
+
if r.status_code != 200:
|
175
|
+
logger.error(f"error during token refresh: {r.content}")
|
176
|
+
raise InvalidRefreshTokenError("refresh token exchange failed")
|
177
|
+
|
178
|
+
# add previous refresh token to new tokens if omitted from resp
|
179
|
+
# (i.e. we have a non-rotating refresh token)
|
180
|
+
resp: dict = r.json()
|
181
|
+
resp.setdefault("refresh_token", refresh_token)
|
182
|
+
|
183
|
+
self._validate_and_save_tokens(resp)
|
184
|
+
|
185
|
+
logger.debug(f"Refreshed tokens: {self._tokens}")
|
186
|
+
return self
|
187
|
+
|
188
|
+
async def async_refresh_if_necessary(
|
189
|
+
self,
|
190
|
+
scope: Optional[str] = None,
|
191
|
+
auto_refresh_threshold: int = 60,
|
192
|
+
):
|
287
193
|
"""
|
288
|
-
|
194
|
+
Refresh the access token if it is expired or bootstrap the access token from a refresh token
|
195
|
+
|
196
|
+
Args:
|
197
|
+
scope: the specific oauth2 scopes to request
|
198
|
+
auto_refresh_threshold: access token TTL remaining (in seconds) before auto-refreshing
|
289
199
|
|
290
|
-
|
291
|
-
|
200
|
+
Raises:
|
201
|
+
NoTokensError: no tokens (at all) are present
|
202
|
+
NoRefreshTokenError: no refresh token is present
|
203
|
+
InvalidRefreshTokenError: the token refresh failed
|
292
204
|
"""
|
293
|
-
|
205
|
+
# Suppress to allow bootstrapping with a refresh token
|
206
|
+
with suppress(NoAccessTokenError):
|
207
|
+
if self.ttl >= dt.timedelta(seconds=auto_refresh_threshold):
|
208
|
+
return self
|
209
|
+
|
210
|
+
return await self.async_refresh(scope=scope)
|
294
211
|
|
295
|
-
|
296
|
-
----------
|
297
|
-
creds : ValidTokens
|
212
|
+
async def async_revoke_refresh_token(self):
|
298
213
|
"""
|
299
|
-
|
214
|
+
Revoke the refresh token.
|
300
215
|
|
301
|
-
|
216
|
+
This invalidates the refresh token server-side so it may never again be used to
|
217
|
+
get new access tokens.
|
218
|
+
|
219
|
+
Raises:
|
220
|
+
NoTokensError: no tokens (at all) are present
|
221
|
+
NoRefreshTokenError: no refresh token is present
|
222
|
+
InvalidRefreshTokenError: the token revocation failed
|
302
223
|
"""
|
303
|
-
|
224
|
+
refresh_token = self.refresh_token
|
304
225
|
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
if not self.has_refresh_token:
|
320
|
-
raise NoRefreshTokenError("No refresh token was found. Please re-authenticate.")
|
321
|
-
|
322
|
-
if revoke:
|
323
|
-
r = requests.post(
|
324
|
-
f"https://{self.auth0_domain}/oauth/revoke",
|
325
|
-
headers={"content-type": "application/json"},
|
326
|
-
data=json.dumps({
|
327
|
-
"client_id": self.auth0_client_id,
|
328
|
-
"token": self.refresh_token,
|
329
|
-
}),
|
330
|
-
)
|
331
|
-
if r.status_code == 200:
|
332
|
-
logger.debug(f"Refresh token revoked: {self.refresh_token}")
|
333
|
-
return
|
334
|
-
else:
|
335
|
-
raise RuntimeError(r.json()["detail"])
|
336
|
-
|
337
|
-
|
338
|
-
else:
|
339
|
-
r = requests.post(
|
340
|
-
f"https://{self.auth0_domain}/oauth/token",
|
341
|
-
headers={"content-type": "application/x-www-form-urlencoded"},
|
342
|
-
data={
|
343
|
-
"grant_type": "refresh_token",
|
344
|
-
"client_id": self.auth0_client_id,
|
345
|
-
"refresh_token": self.refresh_token,
|
346
|
-
"scopes": scope,
|
347
|
-
},
|
348
|
-
)
|
349
|
-
|
350
|
-
# add previous refresh token to new access token
|
351
|
-
if r.status_code == 200:
|
352
|
-
refresh_token = self.refresh_token
|
353
|
-
self.validate_and_save_tokens(r.json())
|
354
|
-
if not self.has_refresh_token:
|
355
|
-
self._tokens.refresh_token = refresh_token
|
356
|
-
self.save_tokens(self._tokens)
|
357
|
-
logger.debug(f"Refreshed tokens: {self._tokens}")
|
358
|
-
return self
|
226
|
+
r = await self._ctx.httpx_client.post(
|
227
|
+
f"{self._settings.domain}oauth/revoke",
|
228
|
+
auth=None, # override client default
|
229
|
+
headers={"content-type": "application/json"},
|
230
|
+
json={
|
231
|
+
"client_id": self._settings.client_id,
|
232
|
+
"token": refresh_token,
|
233
|
+
},
|
234
|
+
)
|
235
|
+
|
236
|
+
if r.status_code != 200:
|
237
|
+
logger.error(f"error while revoking refresh token: {r.content}")
|
238
|
+
raise InvalidRefreshTokenError("refresh token revocation failed")
|
359
239
|
|
360
|
-
|
240
|
+
logger.debug(f"Refresh token revoked: {refresh_token}")
|
241
|
+
return self
|
361
242
|
|
362
|
-
def
|
243
|
+
def _validate_and_save_tokens(self, unvalidated_tokens: dict):
|
363
244
|
"""
|
364
|
-
Validate
|
245
|
+
Validate then save tokens to local storage
|
365
246
|
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
247
|
+
Args:
|
248
|
+
unvalidated_tokens: tokens that have yet to be validated
|
249
|
+
|
250
|
+
Returns:
|
251
|
+
this auth flow
|
252
|
+
|
253
|
+
Raises:
|
254
|
+
pydantic.ValidationError: the token's body could not be decoded
|
370
255
|
"""
|
371
|
-
self.
|
256
|
+
self._validate_tokens(unvalidated_tokens)
|
257
|
+
|
372
258
|
try:
|
373
|
-
self.
|
259
|
+
self._ctx.settings.write_tokens(self._tokens)
|
374
260
|
except Exception as e:
|
375
261
|
logger.error("Error while persisting tokens", exc_info=e)
|
262
|
+
raise
|
376
263
|
|
377
264
|
return self
|
378
265
|
|
379
|
-
def
|
266
|
+
def _validate_tokens(self, unvalidated_tokens: dict):
|
380
267
|
"""
|
381
|
-
Validate the
|
268
|
+
Validate the tokens
|
269
|
+
|
270
|
+
Args:
|
271
|
+
unvalidated_tokens: tokens that have yet to be validated
|
382
272
|
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
273
|
+
Returns:
|
274
|
+
this auth flow
|
275
|
+
|
276
|
+
Raises:
|
277
|
+
pydantic.ValidationError: the token's body could not be decoded
|
387
278
|
"""
|
388
|
-
creds = UnvalidatedTokens.from_dict(unvalidated_creds)
|
389
|
-
idt_body: Optional[Dict] = None
|
390
279
|
|
391
|
-
|
392
|
-
at_body = jwt.decode(
|
393
|
-
creds.access_token,
|
394
|
-
options={"verify_signature": False},
|
395
|
-
)
|
396
|
-
|
397
|
-
if creds.id_token:
|
398
|
-
idt_body = jwt.decode(
|
399
|
-
creds.id_token,
|
400
|
-
options={"verify_signature": False},
|
401
|
-
)
|
402
|
-
except Exception as e:
|
403
|
-
logger.error("Invalid tokens", exc_info=e)
|
404
|
-
raise
|
280
|
+
tokens = Tokens.model_validate(unvalidated_tokens)
|
405
281
|
|
406
|
-
self._access_token_body =
|
407
|
-
self.
|
408
|
-
self._tokens = ValidTokens.from_unvalidated(
|
409
|
-
creds=creds,
|
410
|
-
body=at_body,
|
411
|
-
)
|
412
|
-
self.token_scope = self._tokens.scope
|
282
|
+
self._access_token_body = tokens.access_token_body
|
283
|
+
self._tokens = tokens
|
413
284
|
|
414
285
|
return self
|
415
286
|
|
416
|
-
|
287
|
+
##########
|
288
|
+
# The following methods make this class compatible with httpx.Auth.
|
289
|
+
##########
|
290
|
+
|
291
|
+
async def async_auth_flow(self, request: httpx.Request):
|
292
|
+
"""
|
293
|
+
Injects authorization into the request
|
294
|
+
|
295
|
+
(this method makes this class httpx.Auth compatible)
|
417
296
|
"""
|
418
|
-
|
297
|
+
super().async_auth_flow
|
298
|
+
if request.headers.get("authorization") is None:
|
299
|
+
await self.async_refresh_if_necessary()
|
300
|
+
access_token = self.access_token
|
301
|
+
request.headers["authorization"] = f"Bearer {access_token}"
|
419
302
|
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
The amount of time remaining (in seconds) before token expiration after which a refresh is automatically attempted (default is 3600)
|
303
|
+
yield request
|
304
|
+
|
305
|
+
def sync_auth_flow(self, request: httpx.Request):
|
306
|
+
"""
|
307
|
+
Injects authorization into the request
|
426
308
|
|
427
|
-
|
428
|
-
________
|
429
|
-
access_token: str
|
309
|
+
(this method makes this class httpx.Auth compatible)
|
430
310
|
"""
|
311
|
+
# NOTE: we explicitly redefine this sync method because ctx.syncify()
|
312
|
+
# does not support generators
|
313
|
+
if request.headers.get("authorization") is None:
|
314
|
+
self.refresh_if_necessary()
|
315
|
+
access_token = self.access_token
|
316
|
+
request.headers["authorization"] = f"Bearer {access_token}"
|
431
317
|
|
432
|
-
|
433
|
-
if (
|
434
|
-
not no_auto_refresh
|
435
|
-
and self.ttl < dt.timedelta(seconds=auto_refresh_threshold)
|
436
|
-
):
|
437
|
-
try:
|
438
|
-
self.refresh()
|
439
|
-
except InvalidRefreshTokenError:
|
440
|
-
raise InvalidRefreshTokenError("Unable to refresh because the refresh token is not valid. Use 'no-auto-refresh' option to get token anyway. To resolve, re-authenticate")
|
441
|
-
return self.access_token
|
318
|
+
yield request
|