earthscope-sdk 0.2.0__py3-none-any.whl → 1.0.0b0__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.
- 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
@@ -1,30 +1,14 @@
|
|
1
|
-
from dataclasses import dataclass
|
2
|
-
from pathlib import Path
|
3
|
-
import json
|
4
1
|
import logging
|
5
|
-
import
|
2
|
+
from dataclasses import dataclass
|
3
|
+
from json import JSONDecodeError
|
6
4
|
|
7
|
-
from earthscope_sdk.auth.auth_flow import
|
8
|
-
|
9
|
-
|
10
|
-
NoTokensError,
|
11
|
-
UnauthorizedError,
|
12
|
-
ValidTokens
|
13
|
-
)
|
5
|
+
from earthscope_sdk.auth.auth_flow import AuthFlow
|
6
|
+
from earthscope_sdk.auth.error import ClientCredentialsFlowError, UnauthorizedError
|
7
|
+
from earthscope_sdk.common.context import SdkContext
|
14
8
|
|
15
9
|
logger = logging.getLogger(__name__)
|
16
10
|
|
17
11
|
|
18
|
-
class ClientCredentialsFlowError(AuthFlowError):
|
19
|
-
pass
|
20
|
-
|
21
|
-
|
22
|
-
@dataclass
|
23
|
-
class ClientCredentials:
|
24
|
-
client_id: str
|
25
|
-
client_secret: str
|
26
|
-
|
27
|
-
|
28
12
|
@dataclass
|
29
13
|
class GetTokensErrorResponse:
|
30
14
|
error: str
|
@@ -33,158 +17,64 @@ class GetTokensErrorResponse:
|
|
33
17
|
|
34
18
|
class ClientCredentialsFlow(AuthFlow):
|
35
19
|
"""
|
36
|
-
|
37
|
-
|
38
|
-
Attributes
|
39
|
-
__________
|
40
|
-
audience : str
|
41
|
-
Auth0 API Identifier (default is 'https://account.earthscope.org')
|
42
|
-
domain : str
|
43
|
-
Auth0 tenant domain URL or custom domain (default is 'login.earthscope.org')
|
44
|
-
client_credentials: ClientCredentials
|
45
|
-
The client credentials - client id and client secret
|
46
|
-
_secret: str
|
47
|
-
The client secret of the machine-to-machine application from the client_credentials
|
48
|
-
|
49
|
-
Methods
|
50
|
-
-------
|
51
|
-
request_tokens
|
52
|
-
Request the token from Auth0. Validates and saves the token locally.
|
20
|
+
Implements the oauth2 Client Credentials "machine-to-machine" (M2M) flow.
|
53
21
|
"""
|
54
|
-
def __init__(
|
55
|
-
self, domain: str, audience: str, client_credentials: ClientCredentials
|
56
|
-
) -> None:
|
57
|
-
if not client_credentials or not client_credentials.client_secret:
|
58
|
-
raise ValueError("Client secret missing")
|
59
|
-
|
60
|
-
super().__init__(
|
61
|
-
domain=domain,
|
62
|
-
audience=audience,
|
63
|
-
client_id=client_credentials.client_id,
|
64
|
-
scope="",
|
65
|
-
)
|
66
|
-
# Flow management vars
|
67
|
-
self._secret = client_credentials.client_secret
|
68
22
|
|
69
|
-
def
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
try:
|
75
|
-
self.load_tokens()
|
76
|
-
if self.ttl.total_seconds() <= 0:
|
77
|
-
self.request_tokens()
|
23
|
+
def __init__(self, ctx: SdkContext):
|
24
|
+
if not ctx.settings.oauth2.client_secret:
|
25
|
+
raise ValueError("Client secret required for client credentials flow")
|
26
|
+
|
27
|
+
super().__init__(ctx=ctx)
|
78
28
|
|
79
|
-
|
80
|
-
|
29
|
+
# httpx.Auth objects can be used in either sync or async clients so we
|
30
|
+
# facilitate both from the same class
|
31
|
+
self.request_tokens = ctx.syncify(self.async_request_tokens)
|
81
32
|
|
82
|
-
|
33
|
+
async def async_refresh(self, *args, **kwargs):
|
34
|
+
# alias for self.request_tokens() so that base class can get new tokens automagically
|
35
|
+
return await self.async_request_tokens()
|
83
36
|
|
84
|
-
def
|
37
|
+
async def async_request_tokens(self):
|
85
38
|
"""
|
86
|
-
Request
|
87
|
-
|
88
|
-
Raises
|
89
|
-
|
90
|
-
|
91
|
-
If the request returns as unauthorized
|
92
|
-
ClientCredentialsFlowError
|
93
|
-
If there is an error in the clientcredentials flow other than unauthorized
|
39
|
+
Request access token from IdP
|
40
|
+
|
41
|
+
Raises:
|
42
|
+
Unauthorized Error: the request returns as unauthorized
|
43
|
+
ClientCredentialsFlowError: unhandled error in the client credentials flow
|
94
44
|
"""
|
95
|
-
r =
|
96
|
-
f"
|
45
|
+
r = await self._ctx.httpx_client.post(
|
46
|
+
f"{self._settings.domain}oauth/token",
|
47
|
+
auth=None, # override client default
|
97
48
|
headers={"content-type": "application/x-www-form-urlencoded"},
|
98
49
|
data={
|
99
50
|
"grant_type": "client_credentials",
|
100
|
-
"client_id": self.
|
101
|
-
"client_secret": self.
|
102
|
-
"audience": self.
|
51
|
+
"client_id": self._settings.client_id,
|
52
|
+
"client_secret": self._settings.client_secret.get_secret_value(),
|
53
|
+
"audience": self._settings.audience,
|
103
54
|
},
|
104
55
|
)
|
56
|
+
try:
|
57
|
+
resp: dict = r.json()
|
58
|
+
except JSONDecodeError:
|
59
|
+
raise ClientCredentialsFlowError(
|
60
|
+
f"Unable to unpack IdP response: {r.content}"
|
61
|
+
)
|
62
|
+
|
63
|
+
# Success!
|
105
64
|
if r.status_code == 200:
|
106
|
-
self.
|
65
|
+
self._validate_and_save_tokens(resp)
|
66
|
+
|
107
67
|
logger.debug(f"Got tokens: {self.tokens}")
|
108
68
|
return self
|
109
69
|
|
70
|
+
# Unauthorized
|
110
71
|
if r.status_code == 401:
|
111
|
-
err = GetTokensErrorResponse(**
|
72
|
+
err = GetTokensErrorResponse(**resp)
|
112
73
|
if err.error == "access_denied":
|
113
74
|
if err.error_description == "Unauthorized":
|
114
|
-
raise UnauthorizedError
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
class ClientCredentialsFlowSimple(ClientCredentialsFlow):
|
120
|
-
"""
|
121
|
-
A simple concrete implementation of the ClientCredentialsFlow class. This will store and load tokens on your filesystem
|
122
|
-
|
123
|
-
Attributes:
|
124
|
-
-----------
|
125
|
-
audience : str, optional
|
126
|
-
Auth0 API Identifier (default is 'https://account.earthscope.org')
|
127
|
-
client_credentials: ClientCredentials
|
128
|
-
the client credentials
|
129
|
-
domain : str, optional
|
130
|
-
Auth0 tenant domain URL or custom domain (default is 'login.earthscope.org')
|
131
|
-
path : str
|
132
|
-
The path where your access token will be stored and read
|
133
|
-
|
134
|
-
Methods
|
135
|
-
-------
|
136
|
-
load_tokens
|
137
|
-
Loads and validates your access token from the location specified by the given path.
|
138
|
-
save_tokens(creds)
|
139
|
-
Saves your access token to the location specified by the given path.
|
140
|
-
"""
|
141
|
-
def __init__(
|
142
|
-
self,
|
143
|
-
client_id: str,
|
144
|
-
client_secret: str,
|
145
|
-
path: str,
|
146
|
-
domain: str = "login.earthscope.org",
|
147
|
-
audience: str = "https://account.earthscope.org",
|
148
|
-
) -> None:
|
149
|
-
super().__init__(
|
150
|
-
domain=domain,
|
151
|
-
audience=audience,
|
152
|
-
client_credentials=ClientCredentials(
|
153
|
-
client_id=client_id, client_secret=client_secret
|
154
|
-
),
|
155
|
-
)
|
156
|
-
self.path = Path(path)
|
75
|
+
raise UnauthorizedError(
|
76
|
+
f"m2m client '{self._settings.client_id}' is not authorized"
|
77
|
+
)
|
157
78
|
|
158
|
-
|
159
|
-
""
|
160
|
-
Loads and validates the access token from the location specified by the given path.
|
161
|
-
|
162
|
-
Raises
|
163
|
-
------
|
164
|
-
NoTokensError
|
165
|
-
If no token is found
|
166
|
-
Returns
|
167
|
-
_______
|
168
|
-
valid token : str
|
169
|
-
"""
|
170
|
-
try:
|
171
|
-
with self.path.open() as f:
|
172
|
-
json_state = json.load(f)
|
173
|
-
except FileNotFoundError:
|
174
|
-
raise NoTokensError
|
175
|
-
|
176
|
-
self.validate_tokens(json_state)
|
177
|
-
return self.tokens
|
178
|
-
|
179
|
-
def save_tokens(self, creds: ValidTokens):
|
180
|
-
"""
|
181
|
-
Writes the token to the location specified by the given path in json format.
|
182
|
-
|
183
|
-
Parameters
|
184
|
-
----------
|
185
|
-
creds: ValidTokens
|
186
|
-
token credentials
|
187
|
-
"""
|
188
|
-
json_str_state = json.dumps(vars(creds))
|
189
|
-
with self.path.open("w") as f:
|
190
|
-
f.write(json_str_state)
|
79
|
+
# Unhandled
|
80
|
+
raise ClientCredentialsFlowError("client credentials flow failed", r.content)
|
@@ -1,16 +1,19 @@
|
|
1
|
-
import json
|
2
1
|
import logging
|
3
|
-
import
|
4
|
-
|
5
|
-
from abc import abstractmethod
|
2
|
+
from contextlib import asynccontextmanager, contextmanager
|
6
3
|
from dataclasses import dataclass
|
7
4
|
from enum import Enum
|
8
|
-
from
|
5
|
+
from json import JSONDecodeError
|
9
6
|
from time import sleep
|
10
7
|
from typing import Optional
|
11
8
|
|
12
|
-
from earthscope_sdk.auth.auth_flow import AuthFlow
|
13
|
-
|
9
|
+
from earthscope_sdk.auth.auth_flow import AuthFlow
|
10
|
+
from earthscope_sdk.auth.error import (
|
11
|
+
DeviceCodePollingError,
|
12
|
+
DeviceCodePollingExpiredError,
|
13
|
+
DeviceCodeRequestDeviceCodeError,
|
14
|
+
UnauthorizedError,
|
15
|
+
)
|
16
|
+
from earthscope_sdk.common.context import SdkContext
|
14
17
|
|
15
18
|
logger = logging.getLogger(__name__)
|
16
19
|
|
@@ -22,22 +25,6 @@ class PollingErrorType(str, Enum):
|
|
22
25
|
ACCESS_DENIED = "access_denied"
|
23
26
|
|
24
27
|
|
25
|
-
class RequestDeviceTokensError(RuntimeError):
|
26
|
-
pass
|
27
|
-
|
28
|
-
|
29
|
-
class PollingError(ValueError):
|
30
|
-
pass
|
31
|
-
|
32
|
-
|
33
|
-
class PollingExpiredError(PollingError):
|
34
|
-
pass
|
35
|
-
|
36
|
-
|
37
|
-
class PollingAccessDeniedError(PollingError):
|
38
|
-
pass
|
39
|
-
|
40
|
-
|
41
28
|
@dataclass
|
42
29
|
class GetDeviceCodeResponse:
|
43
30
|
device_code: str
|
@@ -56,239 +43,199 @@ class PollingErrorResponse:
|
|
56
43
|
|
57
44
|
class DeviceCodeFlow(AuthFlow):
|
58
45
|
"""
|
59
|
-
|
60
|
-
|
61
|
-
Atttributes
|
62
|
-
-----------
|
63
|
-
domain : str
|
64
|
-
Auth0 tenant domain URL or custom domain
|
65
|
-
audience : str
|
66
|
-
Auth0 API Identifier
|
67
|
-
client_id : str
|
68
|
-
Identification value of Auth0 Application
|
69
|
-
scope : str
|
70
|
-
The specific actions Auth0 applications can be allowed to do or information that they can request on a user’s behalf
|
71
|
-
_is_polling: bool, optional
|
72
|
-
True if currently polling (default is False)
|
73
|
-
_device_codes, optional
|
74
|
-
The device code (default is None)
|
75
|
-
|
76
|
-
Methods
|
77
|
-
-------
|
78
|
-
do_flow
|
79
|
-
Requests the device code, prompts the user to complete the device code flow, polls while waiting for completion
|
80
|
-
and returns the token
|
81
|
-
poll
|
82
|
-
Polls for completion of the device code flow
|
83
|
-
prompt_user
|
84
|
-
abstract method for prompting the user to complette the device code flow
|
85
|
-
request_device_code
|
86
|
-
requests the device code from Auth0
|
46
|
+
Implements the oauth2 Device Code flow.
|
87
47
|
"""
|
48
|
+
|
88
49
|
@property
|
89
50
|
def polling(self):
|
90
|
-
"
|
51
|
+
"""
|
52
|
+
This instance is in the process of polling for tokens
|
53
|
+
"""
|
91
54
|
return self._is_polling
|
92
55
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
super().__init__(
|
100
|
-
domain=domain,
|
101
|
-
audience=audience,
|
102
|
-
client_id=client_id,
|
103
|
-
scope=scope,
|
104
|
-
)
|
56
|
+
def __init__(self, ctx: SdkContext):
|
57
|
+
if ctx.settings.oauth2.client_secret:
|
58
|
+
raise ValueError("Client secret should not be used with device code flow")
|
59
|
+
|
60
|
+
super().__init__(ctx=ctx)
|
61
|
+
|
105
62
|
# Flow management vars
|
106
63
|
self._is_polling = False
|
107
|
-
self._device_codes: Optional[GetDeviceCodeResponse] = None
|
108
64
|
|
109
|
-
|
110
|
-
|
111
|
-
|
65
|
+
# httpx.Auth objects can be used in either sync or async clients so we
|
66
|
+
# facilitate both from the same class
|
67
|
+
self._poll = ctx.syncify(self._async_poll)
|
68
|
+
self._request_device_code = ctx.syncify(self._async_request_device_code)
|
112
69
|
|
113
|
-
|
114
|
-
|
115
|
-
token
|
70
|
+
@asynccontextmanager
|
71
|
+
async def async_do_flow(self, scope: Optional[str] = None):
|
116
72
|
"""
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
73
|
+
Perform the oauth2 Device Code flow:
|
74
|
+
- requests device code
|
75
|
+
- yields the device code to the caller for them to prompt the user
|
76
|
+
- polls for access token
|
77
|
+
|
78
|
+
Args:
|
79
|
+
scope: the specific oauth2 scopes to request
|
80
|
+
|
81
|
+
Yields:
|
82
|
+
GetDeviceCodeResponse: the device code response that must be shown to the user
|
83
|
+
to facilitate a login.
|
84
|
+
|
85
|
+
Raises:
|
86
|
+
DeviceCodeRequestDeviceCodeError: failed to get a device code from the IdP
|
87
|
+
DeviceCodePollingExpiredError: the polling token expired
|
88
|
+
UnauthorizedError: access denied
|
89
|
+
DeviceCodePollingError: unhandled polling error
|
90
|
+
|
91
|
+
Examples:
|
92
|
+
>>> async with device_flow.async_do_flow() as codes:
|
93
|
+
... # prompt the user to login using the device code
|
94
|
+
... print(f"Open the following URL in a web browser to login: {codes.verification_uri_complete}")
|
95
|
+
"""
|
96
|
+
# NOTE: if you update this method, also update the synchronous version
|
97
|
+
|
98
|
+
codes = await self._async_request_device_code(scope=scope)
|
99
|
+
|
100
|
+
# Yield codes to facilitate prompting the user
|
101
|
+
yield codes
|
121
102
|
|
122
|
-
|
103
|
+
await self._async_poll(codes=codes)
|
104
|
+
|
105
|
+
@contextmanager
|
106
|
+
def do_flow(self, scope: Optional[str] = None):
|
107
|
+
"""
|
108
|
+
Perform the oauth2 Device Code flow:
|
109
|
+
- requests device code
|
110
|
+
- yields the device code to the caller for them to prompt the user
|
111
|
+
- polls for access token
|
112
|
+
|
113
|
+
Args:
|
114
|
+
scope: the specific oauth2 scopes to request
|
115
|
+
|
116
|
+
Yields:
|
117
|
+
GetDeviceCodeResponse: the device code response that must be shown to the user
|
118
|
+
to facilitate a login.
|
119
|
+
|
120
|
+
Raises:
|
121
|
+
DeviceCodeRequestDeviceCodeError: failed to get a device code from the IdP
|
122
|
+
DeviceCodePollingExpiredError: the polling token expired
|
123
|
+
UnauthorizedError: access denied
|
124
|
+
DeviceCodePollingError: unhandled polling error
|
125
|
+
|
126
|
+
Examples:
|
127
|
+
>>> with device_flow.do_flow() as codes:
|
128
|
+
... # prompt the user to login using the device code
|
129
|
+
... print(f"Open the following URL in a web browser to login: {codes.verification_uri_complete}")
|
123
130
|
"""
|
124
|
-
|
131
|
+
# NOTE: we explicitly redefine this sync method because ctx.syncify()
|
132
|
+
# does not support generators
|
133
|
+
|
134
|
+
codes = self._request_device_code(scope=scope)
|
135
|
+
|
136
|
+
# Yield codes to facilitate prompting the user
|
137
|
+
yield codes
|
138
|
+
|
139
|
+
self._poll(codes=codes)
|
140
|
+
|
141
|
+
async def _async_poll(self, codes: GetDeviceCodeResponse):
|
125
142
|
"""
|
126
|
-
|
127
|
-
raise PollingError("Cannot poll without initial device code response")
|
143
|
+
Polling IdP for tokens.
|
128
144
|
|
145
|
+
Args:
|
146
|
+
codes: device code response from IdP after starting the flow
|
147
|
+
|
148
|
+
Raises:
|
149
|
+
DeviceCodePollingExpiredError: the polling token expired
|
150
|
+
UnauthorizedError: access denied
|
151
|
+
DeviceCodePollingError: unhandled polling error
|
152
|
+
"""
|
129
153
|
if self._is_polling:
|
130
|
-
raise
|
154
|
+
raise DeviceCodePollingError("Polling is already in progress")
|
131
155
|
|
132
156
|
self._is_polling = True
|
133
157
|
try:
|
134
158
|
while True:
|
135
|
-
|
159
|
+
# IdP-provided poll interval
|
160
|
+
sleep(codes.interval)
|
136
161
|
|
137
|
-
r =
|
138
|
-
f"
|
162
|
+
r = await self._ctx.httpx_client.post(
|
163
|
+
f"{self._settings.domain}oauth/token",
|
164
|
+
auth=None, # override client default
|
139
165
|
headers={"content-type": "application/x-www-form-urlencoded"},
|
140
166
|
data={
|
141
167
|
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
|
142
|
-
"device_code":
|
143
|
-
"client_id": self.
|
168
|
+
"device_code": codes.device_code,
|
169
|
+
"client_id": self._settings.client_id,
|
144
170
|
},
|
145
171
|
)
|
172
|
+
|
173
|
+
try:
|
174
|
+
resp: dict = r.json()
|
175
|
+
except JSONDecodeError:
|
176
|
+
raise DeviceCodePollingError(
|
177
|
+
f"Unable to unpack IdP response: {r.content}"
|
178
|
+
)
|
179
|
+
|
180
|
+
# Success!
|
146
181
|
if r.status_code == 200:
|
147
|
-
|
148
|
-
|
182
|
+
self._validate_and_save_tokens(resp)
|
183
|
+
|
184
|
+
logger.debug(f"Got tokens: {self.tokens}")
|
149
185
|
return self
|
150
186
|
|
151
|
-
|
187
|
+
# Keep polling
|
188
|
+
poll_err = PollingErrorResponse(**resp)
|
152
189
|
if poll_err.error in [
|
153
190
|
PollingErrorType.AUTHORIZATION_PENDING,
|
154
191
|
PollingErrorType.SLOW_DOWN,
|
155
192
|
]:
|
156
193
|
continue
|
157
194
|
|
195
|
+
# Timeout
|
158
196
|
if poll_err.error == PollingErrorType.EXPIRED_TOKEN:
|
159
|
-
raise
|
197
|
+
raise DeviceCodePollingExpiredError
|
160
198
|
|
199
|
+
# Unauthorized
|
161
200
|
if poll_err.error == PollingErrorType.ACCESS_DENIED:
|
162
|
-
raise
|
201
|
+
raise UnauthorizedError
|
163
202
|
|
203
|
+
# Unhandled
|
164
204
|
if poll_err:
|
165
|
-
raise
|
205
|
+
raise DeviceCodePollingError(f"Unknown polling error: {poll_err}")
|
206
|
+
|
166
207
|
finally:
|
167
208
|
self._is_polling = False
|
168
209
|
|
169
|
-
|
170
|
-
def prompt_user(self):
|
171
|
-
pass
|
172
|
-
|
173
|
-
def request_device_code(self):
|
210
|
+
async def _async_request_device_code(self, scope: Optional[str] = None):
|
174
211
|
"""
|
175
|
-
Request
|
212
|
+
Request new device code from IdP
|
176
213
|
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
214
|
+
Args:
|
215
|
+
scope: the specific oauth2 scopes to request
|
216
|
+
|
217
|
+
Raises:
|
218
|
+
DeviceCodeRequestDeviceCodeError: failed to get a device code from the IdP
|
181
219
|
"""
|
182
|
-
|
183
|
-
|
220
|
+
scope = scope or self._settings.scope
|
221
|
+
|
222
|
+
r = await self._ctx.httpx_client.post(
|
223
|
+
f"{self._settings.domain}oauth/device/code",
|
224
|
+
auth=None, # override client default
|
184
225
|
headers={"content-type": "application/x-www-form-urlencoded"},
|
185
226
|
data={
|
186
|
-
"client_id": self.
|
187
|
-
"scope":
|
188
|
-
"audience": self.
|
227
|
+
"client_id": self._settings.client_id,
|
228
|
+
"scope": scope,
|
229
|
+
"audience": self._settings.audience,
|
189
230
|
},
|
190
231
|
)
|
191
|
-
if r.status_code == 200:
|
192
|
-
self._device_codes = GetDeviceCodeResponse(**r.json())
|
193
|
-
logger.debug(f"Got device code response: {self._device_codes}")
|
194
|
-
return self
|
195
|
-
|
196
|
-
raise RequestDeviceTokensError(f"Failed to get a device code: {r.text}")
|
197
|
-
|
198
232
|
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
and print the user prompt to the standard out.
|
203
|
-
|
204
|
-
Default Auth0 values are for the Production tenant for UNAVCO Data File Server Access Application and User Management API.
|
205
|
-
|
206
|
-
Attributes:
|
207
|
-
-----------
|
208
|
-
path : str
|
209
|
-
The path where your access token will be stored and read. If this is a directory, the file name will be assigned as sso_tokens.json. You maye provide your own file name if desired.
|
210
|
-
domain : str, optional
|
211
|
-
Auth0 tenant domain URL or custom domain (default is 'login.earthscope.org')
|
212
|
-
audience : str, optional
|
213
|
-
Auth0 API Identifier (default is 'https://account.earthscope.org')
|
214
|
-
client_id : str, optional
|
215
|
-
Identification value of Auth0 Application (default is 'b9DtAFBd6QvMg761vI3YhYquNZbJX5G0')
|
216
|
-
scope : str, optional
|
217
|
-
The specific actions Auth0 applications can be allowed to do or information that they can request on a user’s behalf. (default is "openid profile email offline_access")
|
218
|
-
|
219
|
-
Methods
|
220
|
-
-------
|
221
|
-
load_tokens
|
222
|
-
Loads and validates your access token from the location specified by the given path.
|
223
|
-
save_tokens(creds)
|
224
|
-
Saves your access token to the location specified by the given path.
|
225
|
-
prompt_user
|
226
|
-
Prints the link for the sso authorization flow.
|
227
|
-
"""
|
228
|
-
def __init__(
|
229
|
-
self,
|
230
|
-
path: str,
|
231
|
-
domain: str = "login.earthscope.org",
|
232
|
-
audience: str = "https://account.earthscope.org",
|
233
|
-
client_id: str = "b9DtAFBd6QvMg761vI3YhYquNZbJX5G0",
|
234
|
-
scope: str = "openid profile email offline_access"
|
235
|
-
|
236
|
-
) -> None:
|
237
|
-
super().__init__(
|
238
|
-
domain=domain,
|
239
|
-
audience=audience,
|
240
|
-
client_id=client_id,
|
241
|
-
scope=scope,
|
242
|
-
)
|
243
|
-
self.path = Path(path)
|
244
|
-
if self.path.is_dir():
|
245
|
-
self.path = self.path / "sso_tokens.json"
|
246
|
-
|
247
|
-
def load_tokens(self):
|
248
|
-
"""
|
249
|
-
Loads and validates your access token from the location specified by the given path.
|
250
|
-
|
251
|
-
Raises
|
252
|
-
------
|
253
|
-
NoTokensError
|
254
|
-
If no token is found
|
255
|
-
Returns
|
256
|
-
_______
|
257
|
-
valid token : str
|
258
|
-
"""
|
259
|
-
try:
|
260
|
-
with self.path.open() as f:
|
261
|
-
json_state = json.load(f)
|
262
|
-
except FileNotFoundError:
|
263
|
-
raise NoTokensError
|
264
|
-
|
265
|
-
self.validate_tokens(json_state)
|
266
|
-
return self.tokens
|
267
|
-
|
268
|
-
def save_tokens(self, creds: ValidTokens):
|
269
|
-
"""
|
270
|
-
writes your access token to the location specified by the given path in json format.
|
271
|
-
|
272
|
-
Parameters
|
273
|
-
----------
|
274
|
-
creds: ValidTokens
|
275
|
-
token credentials
|
276
|
-
"""
|
277
|
-
json_str_state = json.dumps(vars(creds))
|
278
|
-
with self.path.open("w") as f:
|
279
|
-
f.write(json_str_state)
|
280
|
-
|
281
|
-
def prompt_user(self):
|
282
|
-
"""
|
283
|
-
Prints the url to complete the sso authorization flow to to stdout
|
284
|
-
"""
|
285
|
-
if not self._device_codes:
|
286
|
-
raise RuntimeError(
|
287
|
-
"You must start the device flow before prompting the user"
|
233
|
+
if r.status_code != 200:
|
234
|
+
raise DeviceCodeRequestDeviceCodeError(
|
235
|
+
f"Failed to get a device code: {r.content}"
|
288
236
|
)
|
289
237
|
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
)
|
238
|
+
codes = GetDeviceCodeResponse(**r.json())
|
239
|
+
|
240
|
+
logger.debug(f"Got device code response: {codes}")
|
241
|
+
return codes
|