earthscope-sdk 0.2.1__py3-none-any.whl → 1.0.0__py3-none-any.whl

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 (31) hide show
  1. earthscope_sdk/__init__.py +5 -1
  2. earthscope_sdk/auth/auth_flow.py +240 -346
  3. earthscope_sdk/auth/client_credentials_flow.py +42 -162
  4. earthscope_sdk/auth/device_code_flow.py +169 -213
  5. earthscope_sdk/auth/error.py +46 -0
  6. earthscope_sdk/client/__init__.py +3 -0
  7. earthscope_sdk/client/_client.py +35 -0
  8. earthscope_sdk/client/user/_base.py +39 -0
  9. earthscope_sdk/client/user/_service.py +94 -0
  10. earthscope_sdk/client/user/models.py +53 -0
  11. earthscope_sdk/common/__init__.py +0 -0
  12. earthscope_sdk/common/_sync_runner.py +141 -0
  13. earthscope_sdk/common/client.py +99 -0
  14. earthscope_sdk/common/context.py +174 -0
  15. earthscope_sdk/common/service.py +59 -0
  16. earthscope_sdk/config/__init__.py +0 -0
  17. earthscope_sdk/config/_bootstrap.py +42 -0
  18. earthscope_sdk/config/_compat.py +148 -0
  19. earthscope_sdk/config/_util.py +48 -0
  20. earthscope_sdk/config/error.py +4 -0
  21. earthscope_sdk/config/models.py +310 -0
  22. earthscope_sdk/config/settings.py +295 -0
  23. earthscope_sdk/model/secret.py +29 -0
  24. {earthscope_sdk-0.2.1.dist-info → earthscope_sdk-1.0.0.dist-info}/METADATA +147 -123
  25. earthscope_sdk-1.0.0.dist-info/RECORD +30 -0
  26. {earthscope_sdk-0.2.1.dist-info → earthscope_sdk-1.0.0.dist-info}/WHEEL +1 -1
  27. earthscope_sdk/user/user.py +0 -24
  28. earthscope_sdk-0.2.1.dist-info/RECORD +0 -12
  29. /earthscope_sdk/{user → client/user}/__init__.py +0 -0
  30. {earthscope_sdk-0.2.1.dist-info → earthscope_sdk-1.0.0.dist-info/licenses}/LICENSE +0 -0
  31. {earthscope_sdk-0.2.1.dist-info → earthscope_sdk-1.0.0.dist-info}/top_level.txt +0 -0
@@ -1 +1,5 @@
1
- __version__ = "0.2.1"
1
+ __version__ = "1.0.0"
2
+
3
+ from earthscope_sdk.client import AsyncEarthScopeClient, EarthScopeClient
4
+
5
+ __all__ = ["AsyncEarthScopeClient", "EarthScopeClient"]
@@ -1,441 +1,335 @@
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
- from abc import ABC, abstractmethod
6
- from dataclasses import dataclass
7
- from typing import Dict, Optional, Union
8
- import jwt
6
+ import httpx
7
+ from pydantic import ValidationError
9
8
 
10
- import requests
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 AuthFlowError(Exception):
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
- This is an abstract class that handles retrieving, validating, saving, and refreshing access tokens.
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
- Get access token
124
-
125
- Returns
126
- -------
127
- access token: str
128
- """
129
- return self.tokens.access_token
29
+ Access token
130
30
 
131
- @property
132
- def access_token_body(self):
31
+ Raises:
32
+ NoTokensError: no tokens (at all) are present
33
+ NoAccessTokenError: no access token is present
133
34
  """
134
- Body of the access token
35
+ if at := self.tokens.access_token:
36
+ return at.get_secret_value()
135
37
 
136
- Raises
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 tokens(self):
41
+ def access_token_body(self):
151
42
  """
152
- Get tokens
153
-
154
- raises
155
- ------
156
- NoTokensError
157
- If no token to retrieve
43
+ Access token body
158
44
 
159
- Returns
160
- -------
161
- tokens
45
+ Raises:
46
+ NoAccessTokenError: no access token is present
162
47
  """
163
- if not self._tokens:
164
- raise NoTokensError
48
+ if body := self._access_token_body:
49
+ return body
165
50
 
166
- return self._tokens
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
- When the token expires
56
+ Time of access token expiration
172
57
 
173
- Returns
174
- -------
175
- datetime
58
+ Raises:
59
+ NoAccessTokenError: no access token is present
176
60
  """
177
- return dt.datetime.fromtimestamp(self.tokens.expires_at, dt.timezone.utc)
61
+ return self.access_token_body.expires_at
178
62
 
179
63
  @property
180
- def id_token(self):
64
+ def has_refresh_token(self):
181
65
  """
182
- Get the ID token
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
- idt = self.tokens.id_token
194
- if not idt:
195
- raise NoIdTokenError
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 id_token_body(self):
74
+ def issued_at(self):
201
75
  """
202
- Body of the id token
203
-
204
- Raises
205
- ______
206
- NoIdTokenError
207
- If there is no id token
76
+ Time of access token creation
208
77
 
209
- Returns
210
- -------
211
- id_token_body: dict
78
+ Raises:
79
+ NoAccessTokenError: no access token is present
212
80
  """
213
- if self._id_token_body is None:
214
- raise NoIdTokenError
215
- return self._id_token_body
81
+ return self.access_token_body.issued_at
216
82
 
217
83
  @property
218
- def issued_at(self):
84
+ def refresh_token(self):
219
85
  """
220
- When the token was issued
86
+ Refresh token
221
87
 
222
- Returns
223
- -------
224
- datetime
88
+ Raises:
89
+ NoTokensError: no tokens (at all) are present
90
+ NoRefreshTokenError: no refresh token is present
225
91
  """
226
- return dt.datetime.fromtimestamp(self.tokens.issued_at, dt.timezone.utc)
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 refresh_token(self):
98
+ def scope(self):
230
99
  """
231
- Get the refresh token
100
+ Access token scope
232
101
 
233
- Raises
234
- ------
235
- NoRefreshTokenError
236
- If there is no refresh token to be retrieved
237
-
238
- Returns
239
- -------
240
- refresh token: srt
102
+ Raises:
103
+ NoAccessTokenError: no access token is present
241
104
  """
242
- rt = self.tokens.refresh_token
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 has_refresh_token(self):
108
+ def tokens(self):
250
109
  """
251
- If the acces token contains a refresh token
110
+ Oauth2 tokens managed by this auth flow
252
111
 
253
- Returns
254
- -------
255
- boolean
112
+ Raises:
113
+ NoTokensError: no tokens (at all) are present
256
114
  """
257
- try:
258
- return self.tokens.refresh_token is not None
259
- except NoTokensError:
260
- return False
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
- Time to live of the token
123
+ Access token time-to-live (ttl) before expiration
266
124
 
267
- Returns
268
- -------
269
- datetime
125
+ Raises:
126
+ NoAccessTokenError: no access token is present
270
127
  """
271
- return self.expires_at - dt.datetime.now(dt.timezone.utc)
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
272
133
 
273
- def __init__(self, domain: str, audience: str, client_id: str, scope: str) -> None:
274
- self.auth0_domain = domain
275
- self.auth0_audience = audience
276
- self.auth0_client_id = client_id
277
- self.token_scope = scope
134
+ # Local state
135
+ self._access_token_body: Optional[AccessTokenBody] = None
136
+ self._tokens: Optional[Tokens] = None
278
137
 
279
- self._tokens: Optional[ValidTokens] = None
280
- self._access_token_body: Optional[Dict[str, Union[str, int]]] = None
281
- self._id_token_body: Optional[Dict[str, Union[str, int]]] = None
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)
282
143
 
283
- @abstractmethod
284
- def load_tokens(self):
144
+ # Initialize with provided tokens
145
+ with suppress(ValidationError):
146
+ self._validate_tokens(self._settings)
147
+
148
+ async def async_refresh(self, scope: Optional[str] = None):
285
149
  """
286
- Load the token (abstract method)
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
+ from httpx import HTTPStatusError, ReadTimeout
161
+
162
+ refresh_token = self.refresh_token
163
+ scope = scope or self._settings.scope
164
+
165
+ request = self._ctx.httpx_client.build_request(
166
+ "POST",
167
+ f"{self._settings.domain}oauth/token",
168
+ headers={"content-type": "application/x-www-form-urlencoded"},
169
+ data={
170
+ "grant_type": "refresh_token",
171
+ "client_id": self._settings.client_id,
172
+ "refresh_token": refresh_token,
173
+ "scopes": scope,
174
+ },
175
+ )
176
+
177
+ try:
178
+ async for attempt in self._settings.retry.retry_context(ReadTimeout):
179
+ with attempt:
180
+ r = await self._ctx.httpx_client.send(request, auth=None)
181
+ r.raise_for_status()
182
+ except HTTPStatusError as e:
183
+ logger.error(
184
+ f"error during token refresh ({attempt.num} attempts): {e.response.content}"
185
+ )
186
+ raise InvalidRefreshTokenError("refresh token exchange failed")
187
+ except Exception as e:
188
+ logger.error(
189
+ f"error during token refresh ({attempt.num} attempts)", exc_info=e
190
+ )
191
+ raise InvalidRefreshTokenError("refresh token exchange failed") from e
192
+
193
+ # add previous refresh token to new tokens if omitted from resp
194
+ # (i.e. we have a non-rotating refresh token)
195
+ resp: dict = r.json()
196
+ resp.setdefault("refresh_token", refresh_token)
197
+
198
+ self._validate_and_save_tokens(resp)
199
+
200
+ logger.debug(f"Refreshed tokens: {self._tokens}")
201
+ return self
202
+
203
+ async def async_refresh_if_necessary(
204
+ self,
205
+ scope: Optional[str] = None,
206
+ auto_refresh_threshold: int = 60,
207
+ ):
287
208
  """
288
- pass
209
+ Refresh the access token if it is expired or bootstrap the access token from a refresh token
210
+
211
+ Args:
212
+ scope: the specific oauth2 scopes to request
213
+ auto_refresh_threshold: access token TTL remaining (in seconds) before auto-refreshing
289
214
 
290
- @abstractmethod
291
- def save_tokens(self, creds: ValidTokens):
215
+ Raises:
216
+ NoTokensError: no tokens (at all) are present
217
+ NoRefreshTokenError: no refresh token is present
218
+ InvalidRefreshTokenError: the token refresh failed
292
219
  """
293
- Save the token (abstract method)
220
+ # Suppress to allow bootstrapping with a refresh token
221
+ with suppress(NoAccessTokenError):
222
+ if self.ttl >= dt.timedelta(seconds=auto_refresh_threshold):
223
+ return self
224
+
225
+ return await self.async_refresh(scope=scope)
294
226
 
295
- Parameters
296
- ----------
297
- creds : ValidTokens
227
+ async def async_revoke_refresh_token(self):
298
228
  """
299
- pass
229
+ Revoke the refresh token.
230
+
231
+ This invalidates the refresh token server-side so it may never again be used to
232
+ get new access tokens.
300
233
 
301
- def refresh(self, scope: Optional[str] = None, revoke: bool = False):
234
+ Raises:
235
+ NoTokensError: no tokens (at all) are present
236
+ NoRefreshTokenError: no refresh token is present
237
+ InvalidRefreshTokenError: the token revocation failed
302
238
  """
303
- Refresh the access token
239
+ refresh_token = self.refresh_token
304
240
 
305
- Parameters
306
- __________
307
- scope : str, optional
308
- The specific actions Auth0 applications can be allowed to do or information that they can request on a user’s behalf.
309
- revoke: bool, optional
310
- Whether to revoke the refresh token (default is False)
311
-
312
- Raises
313
- ______
314
- InvalidRefreshTokenError
315
- If the token refresh fails
316
- """
317
- scope = scope or self.token_scope
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
- )
241
+ r = await self._ctx.httpx_client.post(
242
+ f"{self._settings.domain}oauth/revoke",
243
+ auth=None, # override client default
244
+ headers={"content-type": "application/json"},
245
+ json={
246
+ "client_id": self._settings.client_id,
247
+ "token": refresh_token,
248
+ },
249
+ )
349
250
 
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
251
+ if r.status_code != 200:
252
+ logger.error(f"error while revoking refresh token: {r.content}")
253
+ raise InvalidRefreshTokenError("refresh token revocation failed")
359
254
 
360
- raise InvalidRefreshTokenError
255
+ logger.debug(f"Refresh token revoked: {refresh_token}")
256
+ return self
361
257
 
362
- def validate_and_save_tokens(self, unvalidated_creds: Dict):
258
+ def _validate_and_save_tokens(self, unvalidated_tokens: dict):
363
259
  """
364
- Validate and save the token
260
+ Validate then save tokens to local storage
365
261
 
366
- Parameters
367
- __________
368
- unvalidated_creds: dict
369
- The unvalidated credentials to be validated
262
+ Args:
263
+ unvalidated_tokens: tokens that have yet to be validated
264
+
265
+ Returns:
266
+ this auth flow
267
+
268
+ Raises:
269
+ pydantic.ValidationError: the token's body could not be decoded
370
270
  """
371
- self.validate_tokens(unvalidated_creds)
271
+ self._validate_tokens(unvalidated_tokens)
272
+
372
273
  try:
373
- self.save_tokens(self._tokens)
274
+ self._ctx.settings.write_tokens(self._tokens)
374
275
  except Exception as e:
375
276
  logger.error("Error while persisting tokens", exc_info=e)
277
+ raise
376
278
 
377
279
  return self
378
280
 
379
- def validate_tokens(self, unvalidated_creds: Dict):
281
+ def _validate_tokens(self, unvalidated_tokens: dict):
380
282
  """
381
- Validate the token
283
+ Validate the tokens
382
284
 
383
- Paramaters
384
- ----------
385
- unvalidated_creds: dict
386
- The unvalidated credentials to be validated
387
- """
388
- creds = UnvalidatedTokens.from_dict(unvalidated_creds)
389
- idt_body: Optional[Dict] = None
285
+ Args:
286
+ unvalidated_tokens: tokens that have yet to be validated
390
287
 
391
- try:
392
- at_body = jwt.decode(
393
- creds.access_token,
394
- options={"verify_signature": False},
395
- )
288
+ Returns:
289
+ this auth flow
396
290
 
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
291
+ Raises:
292
+ pydantic.ValidationError: the token's body could not be decoded
293
+ """
405
294
 
406
- self._access_token_body = at_body
407
- self._id_token_body = idt_body
408
- self._tokens = ValidTokens.from_unvalidated(
409
- creds=creds,
410
- body=at_body,
411
- )
412
- self.token_scope = self._tokens.scope
295
+ tokens = Tokens.model_validate(unvalidated_tokens)
296
+
297
+ self._access_token_body = tokens.access_token_body
298
+ self._tokens = tokens
413
299
 
414
300
  return self
415
301
 
416
- def get_access_token_refresh_if_necessary(self, no_auto_refresh: bool = False, auto_refresh_threshold: int = 3600):
302
+ ##########
303
+ # The following methods make this class compatible with httpx.Auth.
304
+ ##########
305
+
306
+ async def async_auth_flow(self, request: httpx.Request):
307
+ """
308
+ Injects authorization into the request
309
+
310
+ (this method makes this class httpx.Auth compatible)
417
311
  """
418
- Retrieve the access token. Automatically refreshes the token when necessary.
312
+ super().async_auth_flow
313
+ if request.headers.get("authorization") is None:
314
+ if self._settings.is_host_allowed(request.url.host):
315
+ await self.async_refresh_if_necessary()
316
+ access_token = self.access_token
317
+ request.headers["authorization"] = f"Bearer {access_token}"
419
318
 
420
- Parameters
421
- ___________
422
- no_auto_refresh: bool, optional
423
- Disable automatic token refresh. The default behavior is to automatically attempt to refresh the token if it can be refreshed and there is less than 1 hour before the token expires (default is False)
424
- auto_refresh_threshold: int, optional
425
- The amount of time remaining (in seconds) before token expiration after which a refresh is automatically attempted (default is 3600)
319
+ yield request
320
+
321
+ def sync_auth_flow(self, request: httpx.Request):
322
+ """
323
+ Injects authorization into the request
426
324
 
427
- Return
428
- ________
429
- access_token: str
325
+ (this method makes this class httpx.Auth compatible)
430
326
  """
327
+ # NOTE: we explicitly redefine this sync method because ctx.syncify()
328
+ # does not support generators
329
+ if request.headers.get("authorization") is None:
330
+ if self._settings.is_host_allowed(request.url.host):
331
+ self.refresh_if_necessary()
332
+ access_token = self.access_token
333
+ request.headers["authorization"] = f"Bearer {access_token}"
431
334
 
432
- self.load_tokens()
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
335
+ yield request