earthscope-sdk 1.0.0b0__py3-none-any.whl → 1.1.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.
- earthscope_sdk/__init__.py +1 -1
- earthscope_sdk/auth/auth_flow.py +27 -10
- earthscope_sdk/auth/client_credentials_flow.py +3 -13
- earthscope_sdk/auth/device_code_flow.py +19 -10
- earthscope_sdk/client/user/_base.py +2 -2
- earthscope_sdk/common/context.py +2 -0
- earthscope_sdk/common/service.py +5 -0
- earthscope_sdk/config/_bootstrap.py +42 -0
- earthscope_sdk/config/models.py +125 -22
- earthscope_sdk/config/settings.py +11 -0
- earthscope_sdk/model/secret.py +29 -0
- {earthscope_sdk-1.0.0b0.dist-info → earthscope_sdk-1.1.0.dist-info}/METADATA +9 -6
- earthscope_sdk-1.1.0.dist-info/RECORD +30 -0
- {earthscope_sdk-1.0.0b0.dist-info → earthscope_sdk-1.1.0.dist-info}/WHEEL +1 -1
- earthscope_sdk-1.0.0b0.dist-info/RECORD +0 -28
- {earthscope_sdk-1.0.0b0.dist-info → earthscope_sdk-1.1.0.dist-info/licenses}/LICENSE +0 -0
- {earthscope_sdk-1.0.0b0.dist-info → earthscope_sdk-1.1.0.dist-info}/top_level.txt +0 -0
earthscope_sdk/__init__.py
CHANGED
earthscope_sdk/auth/auth_flow.py
CHANGED
@@ -157,12 +157,14 @@ class AuthFlow(httpx.Auth):
|
|
157
157
|
NoRefreshTokenError: no refresh token is present
|
158
158
|
InvalidRefreshTokenError: the token refresh failed
|
159
159
|
"""
|
160
|
+
from httpx import HTTPStatusError, ReadTimeout
|
161
|
+
|
160
162
|
refresh_token = self.refresh_token
|
161
163
|
scope = scope or self._settings.scope
|
162
164
|
|
163
|
-
|
165
|
+
request = self._ctx.httpx_client.build_request(
|
166
|
+
"POST",
|
164
167
|
f"{self._settings.domain}oauth/token",
|
165
|
-
auth=None, # override client default
|
166
168
|
headers={"content-type": "application/x-www-form-urlencoded"},
|
167
169
|
data={
|
168
170
|
"grant_type": "refresh_token",
|
@@ -171,9 +173,22 @@ class AuthFlow(httpx.Auth):
|
|
171
173
|
"scopes": scope,
|
172
174
|
},
|
173
175
|
)
|
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
|
+
)
|
176
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
|
177
192
|
|
178
193
|
# add previous refresh token to new tokens if omitted from resp
|
179
194
|
# (i.e. we have a non-rotating refresh token)
|
@@ -296,9 +311,10 @@ class AuthFlow(httpx.Auth):
|
|
296
311
|
"""
|
297
312
|
super().async_auth_flow
|
298
313
|
if request.headers.get("authorization") is None:
|
299
|
-
|
300
|
-
|
301
|
-
|
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}"
|
302
318
|
|
303
319
|
yield request
|
304
320
|
|
@@ -311,8 +327,9 @@ class AuthFlow(httpx.Auth):
|
|
311
327
|
# NOTE: we explicitly redefine this sync method because ctx.syncify()
|
312
328
|
# does not support generators
|
313
329
|
if request.headers.get("authorization") is None:
|
314
|
-
self.
|
315
|
-
|
316
|
-
|
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}"
|
317
334
|
|
318
335
|
yield request
|
@@ -1,5 +1,4 @@
|
|
1
1
|
import logging
|
2
|
-
from dataclasses import dataclass
|
3
2
|
from json import JSONDecodeError
|
4
3
|
|
5
4
|
from earthscope_sdk.auth.auth_flow import AuthFlow
|
@@ -9,12 +8,6 @@ from earthscope_sdk.common.context import SdkContext
|
|
9
8
|
logger = logging.getLogger(__name__)
|
10
9
|
|
11
10
|
|
12
|
-
@dataclass
|
13
|
-
class GetTokensErrorResponse:
|
14
|
-
error: str
|
15
|
-
error_description: str
|
16
|
-
|
17
|
-
|
18
11
|
class ClientCredentialsFlow(AuthFlow):
|
19
12
|
"""
|
20
13
|
Implements the oauth2 Client Credentials "machine-to-machine" (M2M) flow.
|
@@ -69,12 +62,9 @@ class ClientCredentialsFlow(AuthFlow):
|
|
69
62
|
|
70
63
|
# Unauthorized
|
71
64
|
if r.status_code == 401:
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
raise UnauthorizedError(
|
76
|
-
f"m2m client '{self._settings.client_id}' is not authorized"
|
77
|
-
)
|
65
|
+
raise UnauthorizedError(
|
66
|
+
f"m2m client '{self._settings.client_id}' is not authorized. IdP response: {resp}"
|
67
|
+
)
|
78
68
|
|
79
69
|
# Unhandled
|
80
70
|
raise ClientCredentialsFlowError("client credentials flow failed", r.content)
|
@@ -1,11 +1,12 @@
|
|
1
1
|
import logging
|
2
|
+
from asyncio import sleep
|
2
3
|
from contextlib import asynccontextmanager, contextmanager
|
3
|
-
from dataclasses import dataclass
|
4
4
|
from enum import Enum
|
5
5
|
from json import JSONDecodeError
|
6
|
-
from time import sleep
|
7
6
|
from typing import Optional
|
8
7
|
|
8
|
+
from pydantic import BaseModel, ValidationError
|
9
|
+
|
9
10
|
from earthscope_sdk.auth.auth_flow import AuthFlow
|
10
11
|
from earthscope_sdk.auth.error import (
|
11
12
|
DeviceCodePollingError,
|
@@ -25,18 +26,16 @@ class PollingErrorType(str, Enum):
|
|
25
26
|
ACCESS_DENIED = "access_denied"
|
26
27
|
|
27
28
|
|
28
|
-
|
29
|
-
class GetDeviceCodeResponse:
|
29
|
+
class GetDeviceCodeResponse(BaseModel):
|
30
30
|
device_code: str
|
31
31
|
user_code: str
|
32
32
|
verification_uri: str
|
33
33
|
verification_uri_complete: str
|
34
34
|
expires_in: int
|
35
|
-
interval:
|
35
|
+
interval: float
|
36
36
|
|
37
37
|
|
38
|
-
|
39
|
-
class PollingErrorResponse:
|
38
|
+
class PollingErrorResponse(BaseModel):
|
40
39
|
error: PollingErrorType
|
41
40
|
error_description: str
|
42
41
|
|
@@ -157,7 +156,7 @@ class DeviceCodeFlow(AuthFlow):
|
|
157
156
|
try:
|
158
157
|
while True:
|
159
158
|
# IdP-provided poll interval
|
160
|
-
sleep(codes.interval)
|
159
|
+
await sleep(codes.interval)
|
161
160
|
|
162
161
|
r = await self._ctx.httpx_client.post(
|
163
162
|
f"{self._settings.domain}oauth/token",
|
@@ -185,7 +184,12 @@ class DeviceCodeFlow(AuthFlow):
|
|
185
184
|
return self
|
186
185
|
|
187
186
|
# Keep polling
|
188
|
-
|
187
|
+
try:
|
188
|
+
poll_err = PollingErrorResponse.model_validate(resp)
|
189
|
+
except ValidationError as e:
|
190
|
+
raise DeviceCodePollingError(
|
191
|
+
f"Failed to unpack polling response: {r.text}"
|
192
|
+
) from e
|
189
193
|
if poll_err.error in [
|
190
194
|
PollingErrorType.AUTHORIZATION_PENDING,
|
191
195
|
PollingErrorType.SLOW_DOWN,
|
@@ -235,7 +239,12 @@ class DeviceCodeFlow(AuthFlow):
|
|
235
239
|
f"Failed to get a device code: {r.content}"
|
236
240
|
)
|
237
241
|
|
238
|
-
|
242
|
+
try:
|
243
|
+
codes = GetDeviceCodeResponse.model_validate_json(r.content)
|
244
|
+
except ValidationError as e:
|
245
|
+
raise DeviceCodeRequestDeviceCodeError(
|
246
|
+
f"Failed to unpack device code response: {r.text}"
|
247
|
+
) from e
|
239
248
|
|
240
249
|
logger.debug(f"Got device code response: {codes}")
|
241
250
|
return codes
|
@@ -18,7 +18,7 @@ class UserBaseService(SdkService):
|
|
18
18
|
url=f"{self.resources.api_url}beta/user/credentials/aws/{role}",
|
19
19
|
)
|
20
20
|
|
21
|
-
resp = await self.
|
21
|
+
resp = await self._send_with_retries(req)
|
22
22
|
|
23
23
|
return AwsTemporaryCredentials.model_validate_json(resp.content)
|
24
24
|
|
@@ -34,6 +34,6 @@ class UserBaseService(SdkService):
|
|
34
34
|
url=f"{self.resources.api_url}beta/user/profile",
|
35
35
|
)
|
36
36
|
|
37
|
-
resp = await self.
|
37
|
+
resp = await self._send_with_retries(req)
|
38
38
|
|
39
39
|
return UserProfile.model_validate_json(resp.content)
|
earthscope_sdk/common/context.py
CHANGED
@@ -78,6 +78,8 @@ class SdkContext:
|
|
78
78
|
self._httpx_client = httpx.AsyncClient(
|
79
79
|
auth=self.auth_flow,
|
80
80
|
headers={
|
81
|
+
**self.settings.http.extra_headers,
|
82
|
+
# override anything specified via extra_headers
|
81
83
|
"user-agent": self.settings.http.user_agent,
|
82
84
|
},
|
83
85
|
limits=self.settings.http.limits,
|
earthscope_sdk/common/service.py
CHANGED
@@ -0,0 +1,42 @@
|
|
1
|
+
"""
|
2
|
+
This module facilitates bootstrapping SDK settings from a JSON-encoded environment variable.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import json
|
6
|
+
import logging
|
7
|
+
import os
|
8
|
+
|
9
|
+
from pydantic_settings import PydanticBaseSettingsSource
|
10
|
+
|
11
|
+
logger = logging.getLogger(__name__)
|
12
|
+
|
13
|
+
|
14
|
+
class BootstrapEnvironmentSettingsSource(PydanticBaseSettingsSource):
|
15
|
+
"""
|
16
|
+
This SettingsSource facilitates bootstrapping the SDK from a special environment variable.
|
17
|
+
|
18
|
+
The environment variable should be a JSON string of the expected SDK settings and structure.
|
19
|
+
"""
|
20
|
+
|
21
|
+
def __init__(self, settings_cls, env_var: str):
|
22
|
+
super().__init__(settings_cls)
|
23
|
+
self._env_var = env_var
|
24
|
+
|
25
|
+
def __call__(self):
|
26
|
+
try:
|
27
|
+
bootstrap_settings = os.environ[self._env_var]
|
28
|
+
except KeyError:
|
29
|
+
return {}
|
30
|
+
|
31
|
+
try:
|
32
|
+
return json.loads(bootstrap_settings)
|
33
|
+
except json.JSONDecodeError:
|
34
|
+
logger.warning(
|
35
|
+
f"Found bootstrap environment variable '{self._env_var}', but unable to decode content as JSON"
|
36
|
+
)
|
37
|
+
return {}
|
38
|
+
|
39
|
+
def __repr__(self) -> str:
|
40
|
+
return f"{self.__class__.__name__}(env_var='{self._env_var}')"
|
41
|
+
|
42
|
+
def get_field_value(self, *args, **kwargs): ... # unused abstract method
|
earthscope_sdk/config/models.py
CHANGED
@@ -1,11 +1,14 @@
|
|
1
1
|
import base64
|
2
2
|
import binascii
|
3
3
|
import datetime as dt
|
4
|
+
import fnmatch
|
5
|
+
import functools
|
4
6
|
from contextlib import suppress
|
5
7
|
from enum import Enum
|
6
8
|
from functools import cached_property
|
7
|
-
from typing import Annotated, Any, Optional, Union
|
9
|
+
from typing import Annotated, Any, Optional, Type, Union
|
8
10
|
|
11
|
+
from annotated_types import Ge, Gt
|
9
12
|
from pydantic import (
|
10
13
|
AliasChoices,
|
11
14
|
BaseModel,
|
@@ -13,14 +16,12 @@ from pydantic import (
|
|
13
16
|
ConfigDict,
|
14
17
|
Field,
|
15
18
|
HttpUrl,
|
16
|
-
SecretStr,
|
17
|
-
SerializationInfo,
|
18
19
|
ValidationError,
|
19
|
-
field_serializer,
|
20
20
|
model_validator,
|
21
21
|
)
|
22
22
|
|
23
23
|
from earthscope_sdk import __version__
|
24
|
+
from earthscope_sdk.model.secret import SecretStr
|
24
25
|
|
25
26
|
|
26
27
|
def _try_float(v: Any):
|
@@ -86,29 +87,12 @@ class Tokens(BaseModel):
|
|
86
87
|
return None
|
87
88
|
|
88
89
|
with suppress(IndexError, binascii.Error, ValidationError):
|
89
|
-
payload_b64 = self.access_token.get_secret_value().split(".")[1]
|
90
|
+
payload_b64 = self.access_token.get_secret_value().split(".", 2)[1]
|
90
91
|
payload = base64.b64decode(payload_b64 + "==") # extra padding
|
91
92
|
return AccessTokenBody.model_validate_json(payload)
|
92
93
|
|
93
94
|
raise ValueError("Unable to decode access token body")
|
94
95
|
|
95
|
-
@field_serializer("access_token", "id_token", "refresh_token", when_used="json")
|
96
|
-
def dump_secret_json(self, secret: Optional[SecretStr], info: SerializationInfo):
|
97
|
-
"""
|
98
|
-
A special field serializer to dump the actual secret value when writing to JSON.
|
99
|
-
|
100
|
-
Only writes secret in plaintext when `info.context == "plaintext".
|
101
|
-
|
102
|
-
See [Pydantic docs](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context)
|
103
|
-
"""
|
104
|
-
if secret is None:
|
105
|
-
return None
|
106
|
-
|
107
|
-
if info.context == "plaintext":
|
108
|
-
return secret.get_secret_value()
|
109
|
-
|
110
|
-
return str(secret)
|
111
|
-
|
112
96
|
@model_validator(mode="after")
|
113
97
|
def ensure_one_of(self):
|
114
98
|
# allow all fields to be optional in subclasses
|
@@ -121,6 +105,76 @@ class Tokens(BaseModel):
|
|
121
105
|
raise ValueError("At least one of access token and refresh token is required.")
|
122
106
|
|
123
107
|
|
108
|
+
class RetrySettings(BaseModel):
|
109
|
+
"""
|
110
|
+
Retry configuration for the [Stamina library](https://stamina.hynek.me/en/stable/index.html)
|
111
|
+
"""
|
112
|
+
|
113
|
+
# same defaults as AWS SDK "standard" mode:
|
114
|
+
# https://boto3.amazonaws.com/v1/documentation/api/latest/guide/retries.html#standard-retry-mode
|
115
|
+
|
116
|
+
attempts: Annotated[int, Ge(0)] = 3
|
117
|
+
timeout: Timedelta = dt.timedelta(seconds=20)
|
118
|
+
|
119
|
+
wait_initial: Timedelta = dt.timedelta(milliseconds=100)
|
120
|
+
wait_max: Timedelta = dt.timedelta(seconds=5)
|
121
|
+
wait_jitter: Timedelta = dt.timedelta(seconds=1)
|
122
|
+
wait_exp_base: Annotated[float, Gt(0)] = 2
|
123
|
+
|
124
|
+
async def retry_context(self, *retry_exc: Type[Exception]):
|
125
|
+
"""
|
126
|
+
Obtain a [Stamina](https://stamina.hynek.me/en/stable/index.html) retry iterator.
|
127
|
+
"""
|
128
|
+
from stamina import retry_context
|
129
|
+
|
130
|
+
retry_on = functools.partial(self.is_retriable, retry_exc=retry_exc)
|
131
|
+
|
132
|
+
ctx = retry_context(
|
133
|
+
on=retry_on,
|
134
|
+
attempts=self.attempts,
|
135
|
+
timeout=self.timeout,
|
136
|
+
wait_initial=self.wait_initial,
|
137
|
+
wait_jitter=self.wait_jitter,
|
138
|
+
wait_max=self.wait_max,
|
139
|
+
wait_exp_base=self.wait_exp_base,
|
140
|
+
)
|
141
|
+
async for attempt in ctx:
|
142
|
+
yield attempt
|
143
|
+
|
144
|
+
def is_retriable(
|
145
|
+
self,
|
146
|
+
exc: Exception,
|
147
|
+
*args,
|
148
|
+
retry_exc: tuple[Type[Exception]] = (),
|
149
|
+
**kwargs,
|
150
|
+
) -> bool:
|
151
|
+
"""
|
152
|
+
Check if the given exception can be retried
|
153
|
+
"""
|
154
|
+
if retry_exc and isinstance(exc, retry_exc):
|
155
|
+
return True
|
156
|
+
|
157
|
+
return False
|
158
|
+
|
159
|
+
|
160
|
+
class HttpRetrySettings(RetrySettings):
|
161
|
+
status_codes: set[int] = {429, 500, 502, 503, 504}
|
162
|
+
|
163
|
+
def is_retriable(
|
164
|
+
self,
|
165
|
+
exc: Exception,
|
166
|
+
*args,
|
167
|
+
**kwargs,
|
168
|
+
) -> bool:
|
169
|
+
from httpx import HTTPStatusError
|
170
|
+
|
171
|
+
if isinstance(exc, HTTPStatusError):
|
172
|
+
if exc.response.status_code in self.status_codes:
|
173
|
+
return True
|
174
|
+
|
175
|
+
return super().is_retriable(exc, *args, **kwargs)
|
176
|
+
|
177
|
+
|
124
178
|
class AuthFlowSettings(Tokens):
|
125
179
|
"""
|
126
180
|
Auth flow configuration
|
@@ -135,6 +189,20 @@ class AuthFlowSettings(Tokens):
|
|
135
189
|
scope: str = "offline_access"
|
136
190
|
client_secret: Optional[SecretStr] = None
|
137
191
|
|
192
|
+
# Only inject bearer token for requests to these hosts
|
193
|
+
allowed_hosts: set[str] = {
|
194
|
+
"earthscope.org",
|
195
|
+
"*.earthscope.org",
|
196
|
+
}
|
197
|
+
|
198
|
+
# Auth exchange retries
|
199
|
+
retry: HttpRetrySettings = HttpRetrySettings(
|
200
|
+
attempts=5,
|
201
|
+
timeout=dt.timedelta(seconds=30),
|
202
|
+
wait_initial=dt.timedelta(seconds=1),
|
203
|
+
wait_jitter=dt.timedelta(seconds=3),
|
204
|
+
)
|
205
|
+
|
138
206
|
@cached_property
|
139
207
|
def auth_flow_type(self) -> AuthFlowType:
|
140
208
|
if self.client_secret is not None:
|
@@ -142,6 +210,37 @@ class AuthFlowSettings(Tokens):
|
|
142
210
|
|
143
211
|
return AuthFlowType.DeviceCode
|
144
212
|
|
213
|
+
@cached_property
|
214
|
+
def allowed_host_patterns(self) -> set[str]:
|
215
|
+
"""
|
216
|
+
The subset of allowed hosts that are glob patterns.
|
217
|
+
|
218
|
+
Use `is_host_allowed` to check if a host is allowed by any of these patterns.
|
219
|
+
"""
|
220
|
+
return {h for h in self.allowed_hosts if "*" in h or "?" in h}
|
221
|
+
|
222
|
+
def is_host_allowed(self, host: str) -> bool:
|
223
|
+
"""
|
224
|
+
Check if a host matches any pattern in the allowed hosts set.
|
225
|
+
|
226
|
+
Supports glob patterns with '?' and '*' characters (e.g., *.earthscope.org).
|
227
|
+
|
228
|
+
Args:
|
229
|
+
host: The hostname to check
|
230
|
+
|
231
|
+
Returns:
|
232
|
+
True if the host matches any allowed pattern, False otherwise
|
233
|
+
"""
|
234
|
+
if host in self.allowed_hosts:
|
235
|
+
return True
|
236
|
+
|
237
|
+
for allowed_pattern in self.allowed_host_patterns:
|
238
|
+
if fnmatch.fnmatch(host, allowed_pattern):
|
239
|
+
self.allowed_hosts.add(host)
|
240
|
+
return True
|
241
|
+
|
242
|
+
return False
|
243
|
+
|
145
244
|
|
146
245
|
class HttpSettings(BaseModel):
|
147
246
|
"""
|
@@ -157,8 +256,12 @@ class HttpSettings(BaseModel):
|
|
157
256
|
timeout_connect: Timedelta = dt.timedelta(seconds=5)
|
158
257
|
timeout_read: Timedelta = dt.timedelta(seconds=5)
|
159
258
|
|
259
|
+
# automatically retry requests
|
260
|
+
retry: HttpRetrySettings = HttpRetrySettings()
|
261
|
+
|
160
262
|
# Other
|
161
263
|
user_agent: str = f"earthscope-sdk py/{__version__}"
|
264
|
+
extra_headers: dict[str, str] = {}
|
162
265
|
|
163
266
|
@cached_property
|
164
267
|
def limits(self):
|
@@ -11,11 +11,15 @@ from pydantic_settings import (
|
|
11
11
|
TomlConfigSettingsSource,
|
12
12
|
)
|
13
13
|
|
14
|
+
from earthscope_sdk.config._bootstrap import BootstrapEnvironmentSettingsSource
|
14
15
|
from earthscope_sdk.config._compat import LegacyEarthScopeCLISettingsSource
|
15
16
|
from earthscope_sdk.config._util import deep_merge, get_config_dir, slugify
|
16
17
|
from earthscope_sdk.config.error import ProfileDoesNotExistError
|
17
18
|
from earthscope_sdk.config.models import SdkBaseSettings, Tokens
|
18
19
|
|
20
|
+
_BOOTSTRAP_ENV_VAR = "ES_BOOTSTRAP_SETTINGS"
|
21
|
+
"""Environment variable for bootstrapping the SDK"""
|
22
|
+
|
19
23
|
_DEFAULT_PROFILE = "default"
|
20
24
|
"""Default profile name"""
|
21
25
|
|
@@ -269,6 +273,12 @@ class SdkSettings(SdkBaseSettings, BaseSettings):
|
|
269
273
|
alias = SdkSettings.model_fields["profile_name"].validation_alias
|
270
274
|
global_settings = _GlobalSettingsSource(settings_cls, "profile_name", alias)
|
271
275
|
|
276
|
+
# Check for bootstrapping configuration
|
277
|
+
bootstrap_settings = BootstrapEnvironmentSettingsSource(
|
278
|
+
settings_cls,
|
279
|
+
_BOOTSTRAP_ENV_VAR,
|
280
|
+
)
|
281
|
+
|
272
282
|
# Compatibility with earthscope-cli v0.x.x state:
|
273
283
|
# If we find this file, we only care about the access and refresh tokens
|
274
284
|
keep_keys = {"access_token", "refresh_token"}
|
@@ -281,4 +291,5 @@ class SdkSettings(SdkBaseSettings, BaseSettings):
|
|
281
291
|
dotenv_settings,
|
282
292
|
global_settings,
|
283
293
|
legacy_settings,
|
294
|
+
bootstrap_settings,
|
284
295
|
)
|
@@ -0,0 +1,29 @@
|
|
1
|
+
from typing import Annotated
|
2
|
+
|
3
|
+
from pydantic import PlainSerializer, SerializationInfo
|
4
|
+
from pydantic import SecretStr as _SecretStr
|
5
|
+
|
6
|
+
|
7
|
+
def _dump_secret_plaintext(secret: _SecretStr, info: SerializationInfo):
|
8
|
+
"""
|
9
|
+
A special field serializer to dump the actual secret value.
|
10
|
+
|
11
|
+
Only writes secret in plaintext when `info.context == "plaintext".
|
12
|
+
|
13
|
+
See [Pydantic docs](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context)
|
14
|
+
"""
|
15
|
+
|
16
|
+
if info.context == "plaintext":
|
17
|
+
return secret.get_secret_value()
|
18
|
+
|
19
|
+
return str(secret)
|
20
|
+
|
21
|
+
|
22
|
+
SecretStr = Annotated[
|
23
|
+
_SecretStr,
|
24
|
+
PlainSerializer(
|
25
|
+
_dump_secret_plaintext,
|
26
|
+
return_type=str,
|
27
|
+
when_used="json-unless-none",
|
28
|
+
),
|
29
|
+
]
|
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: earthscope-sdk
|
3
|
-
Version: 1.0
|
3
|
+
Version: 1.1.0
|
4
4
|
Summary: An SDK for EarthScope API
|
5
5
|
Author-email: EarthScope <data-help@earthscope.org>
|
6
6
|
License: Apache License
|
@@ -214,16 +214,19 @@ Requires-Python: >=3.9
|
|
214
214
|
Description-Content-Type: text/markdown
|
215
215
|
License-File: LICENSE
|
216
216
|
Requires-Dist: httpx>=0.27.0
|
217
|
-
Requires-Dist: pydantic-settings[toml]>=2.
|
217
|
+
Requires-Dist: pydantic-settings[toml]>=2.8.0
|
218
|
+
Requires-Dist: stamina>=24.3.0
|
218
219
|
Provides-Extra: dev
|
219
220
|
Requires-Dist: bumpver; extra == "dev"
|
220
221
|
Requires-Dist: build; extra == "dev"
|
221
222
|
Requires-Dist: pytest; extra == "dev"
|
222
223
|
Requires-Dist: twine; extra == "dev"
|
223
224
|
Requires-Dist: pip-tools; extra == "dev"
|
225
|
+
Requires-Dist: pre-commit; extra == "dev"
|
224
226
|
Requires-Dist: pytest-httpx; extra == "dev"
|
225
227
|
Requires-Dist: pytest-asyncio; extra == "dev"
|
226
228
|
Requires-Dist: ruff; extra == "dev"
|
229
|
+
Dynamic: license-file
|
227
230
|
|
228
231
|
# EarthScope SDK
|
229
232
|
|
@@ -241,7 +244,7 @@ pip install earthscope-sdk
|
|
241
244
|
|
242
245
|
### Usage
|
243
246
|
|
244
|
-
For detailed usage
|
247
|
+
For detailed usage info and examples, visit [our SDK docs](https://docs.earthscope.org/projects/SDK).
|
245
248
|
|
246
249
|
```py
|
247
250
|
# Import and create a client
|
@@ -298,7 +301,7 @@ Once refreshable credentials are available to the SDK, it will transparently han
|
|
298
301
|
|
299
302
|
### Same host
|
300
303
|
|
301
|
-
If you have the [EarthScope CLI](
|
304
|
+
If you have the [EarthScope CLI](https://docs.earthscope.org/projects/CLI) installed on the same host that is running your application which uses `earthscope-sdk`, you can simply log in using the CLI. The CLI shares credentials and configuration with this SDK (when running on the same host).
|
302
305
|
|
303
306
|
Running `es login` will open your browser and prompt you to log in to your EarthScope account.
|
304
307
|
|
@@ -318,7 +321,7 @@ Now when you run your application, `earthscope-sdk` will find your credentials.
|
|
318
321
|
|
319
322
|
Sometimes your workload runs on different hosts than your main workstation and you cannot feasibly "log in" on all of them. For example, maybe you're running many containers in your workload.
|
320
323
|
|
321
|
-
You can still use the [EarthScope CLI](
|
324
|
+
You can still use the [EarthScope CLI](https://docs.earthscope.org/projects/CLI) to facilitate auth for applications on other machines.
|
322
325
|
|
323
326
|
1. Use the CLI on your primary workstation [as described above](#same-host) to log in.
|
324
327
|
|
@@ -0,0 +1,30 @@
|
|
1
|
+
earthscope_sdk/__init__.py,sha256=bkZcCw9euoFA9FOzEZW4H6cpa-EPH1GUsAsUEwLclIY,154
|
2
|
+
earthscope_sdk/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
+
earthscope_sdk/auth/auth_flow.py,sha256=gxJmCr5TAhMEss7RskYFCv6-D3A5h0Zw2ejeLJJ_9aI,10352
|
4
|
+
earthscope_sdk/auth/client_credentials_flow.py,sha256=GAvskuoEd6qHe-BtfGwYQisMedmSkDhagy4aRPVPri4,2494
|
5
|
+
earthscope_sdk/auth/device_code_flow.py,sha256=p53pgRQraToFwONPBj3O3DHAa4x4rE6rvygo_Bbj5_U,8394
|
6
|
+
earthscope_sdk/auth/error.py,sha256=eC33Bw1HaBEJE7-eI2krtE__5PxStc3EyiYO12v0kVw,693
|
7
|
+
earthscope_sdk/client/__init__.py,sha256=JotTr5oTiiOsUc0RTg82EVCUSg_-u80Qu_R0-crCXkY,139
|
8
|
+
earthscope_sdk/client/_client.py,sha256=ai7WdsTOYglA6bLkT-Wntvxlke6nSaGHwqrtg5PEy80,833
|
9
|
+
earthscope_sdk/client/user/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
+
earthscope_sdk/client/user/_base.py,sha256=8dn4pfQMwVDpF0E6dl6P6HJuNVvozUzfgUGefnPXMnw,1076
|
11
|
+
earthscope_sdk/client/user/_service.py,sha256=wRktOZF5GXajXXxij3Nkule6wvuWOV0vn4QsA1IXVHc,3063
|
12
|
+
earthscope_sdk/client/user/models.py,sha256=drZAMwOYC1NVCzBZQhNL-pPTB28SURKfoZF8HdjlIj8,1214
|
13
|
+
earthscope_sdk/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
14
|
+
earthscope_sdk/common/_sync_runner.py,sha256=h_A2pSEjZCLj7ov50M6cWHVoX6eXVmGzz5nX0MwLWDY,4131
|
15
|
+
earthscope_sdk/common/client.py,sha256=g5ZTNhFm33H68J9pWD5fDu760Yd5cBdfQmsbU3t8D_4,2156
|
16
|
+
earthscope_sdk/common/context.py,sha256=bt2UhSsIZGBaukA0QJiFsPhJaSUDdcT9kmAZrfTQsc4,5254
|
17
|
+
earthscope_sdk/common/service.py,sha256=SCUZVJA3jFaEPeFrOf0v9osf2UpqldhlFmirOYWJjxM,1506
|
18
|
+
earthscope_sdk/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
19
|
+
earthscope_sdk/config/_bootstrap.py,sha256=BvDSRccvFPFRnLS4MlE7OMLGYEgxY7znxFZ6AYgxvOE,1243
|
20
|
+
earthscope_sdk/config/_compat.py,sha256=P3F5y_Kf5zp9m9uOhl1Bp3ke6expxq4Sm9AeVaBbAHk,4610
|
21
|
+
earthscope_sdk/config/_util.py,sha256=RZ6zvKrvjUkO7i69s7AVoIDhamRg4x71CAZLnucr9QM,1249
|
22
|
+
earthscope_sdk/config/error.py,sha256=jh25q-b317lAvp32WwQw0zdYoV-MxZtg-n5FgZOMymI,95
|
23
|
+
earthscope_sdk/config/models.py,sha256=MCN9fbJKqnKHnSeg-KhUOsnXsqMxmawcOiZ62689ZS0,8723
|
24
|
+
earthscope_sdk/config/settings.py,sha256=kGsoqAgoUS2xrI0rvdqbLEsI2M0Mpbm73oEDLpJG4_Q,9205
|
25
|
+
earthscope_sdk/model/secret.py,sha256=QTyWCqXvf9ZYWaVVQcGzdt3rGtyU3sx13AlzkNE3gaU,731
|
26
|
+
earthscope_sdk-1.1.0.dist-info/licenses/LICENSE,sha256=E_MrVXxRaMQNpvZhsDuz_J9N_ux7dlL_WpYSsE391HU,11349
|
27
|
+
earthscope_sdk-1.1.0.dist-info/METADATA,sha256=t9dLQDJbJeZBd-CiUncz2Hh4JnGazSsZg19MnIP6KQ8,18122
|
28
|
+
earthscope_sdk-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
29
|
+
earthscope_sdk-1.1.0.dist-info/top_level.txt,sha256=zTtIT9yN3JPJF7TqmTzqQcAvZZe4pAm907DLoGa5T_E,15
|
30
|
+
earthscope_sdk-1.1.0.dist-info/RECORD,,
|
@@ -1,28 +0,0 @@
|
|
1
|
-
earthscope_sdk/__init__.py,sha256=6InyrqE0KEsb_XBBKCbUIb8s0LTJ6N20HFsrO-rHVtI,156
|
2
|
-
earthscope_sdk/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
-
earthscope_sdk/auth/auth_flow.py,sha256=HZyLrt8o3I-0KC7XRg9W0n2NAVXX7EOl9pG-5blv7sA,9613
|
4
|
-
earthscope_sdk/auth/client_credentials_flow.py,sha256=1GyDSIR1OgYP4u0xZoTov1u_YhY1AzHFpOcBCzY1h6E,2769
|
5
|
-
earthscope_sdk/auth/device_code_flow.py,sha256=dC5Ffj3HzBguRxSHCZYvTe1MD3C-iKf2AlanGuRKNvI,7922
|
6
|
-
earthscope_sdk/auth/error.py,sha256=eC33Bw1HaBEJE7-eI2krtE__5PxStc3EyiYO12v0kVw,693
|
7
|
-
earthscope_sdk/client/__init__.py,sha256=JotTr5oTiiOsUc0RTg82EVCUSg_-u80Qu_R0-crCXkY,139
|
8
|
-
earthscope_sdk/client/_client.py,sha256=ai7WdsTOYglA6bLkT-Wntvxlke6nSaGHwqrtg5PEy80,833
|
9
|
-
earthscope_sdk/client/user/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
-
earthscope_sdk/client/user/_base.py,sha256=nut1Ojsksohqy3X3L5FPDQ-rh-BmHLJ6sId5xVqLal0,1050
|
11
|
-
earthscope_sdk/client/user/_service.py,sha256=wRktOZF5GXajXXxij3Nkule6wvuWOV0vn4QsA1IXVHc,3063
|
12
|
-
earthscope_sdk/client/user/models.py,sha256=drZAMwOYC1NVCzBZQhNL-pPTB28SURKfoZF8HdjlIj8,1214
|
13
|
-
earthscope_sdk/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
14
|
-
earthscope_sdk/common/_sync_runner.py,sha256=h_A2pSEjZCLj7ov50M6cWHVoX6eXVmGzz5nX0MwLWDY,4131
|
15
|
-
earthscope_sdk/common/client.py,sha256=g5ZTNhFm33H68J9pWD5fDu760Yd5cBdfQmsbU3t8D_4,2156
|
16
|
-
earthscope_sdk/common/context.py,sha256=vrCB_Ez-98Ir7c0GrCe-g7DuRCgc9vPaoRWFYf5q8Ko,5138
|
17
|
-
earthscope_sdk/common/service.py,sha256=qBz6OV8rQf3WQojubEVfQ4HYeeKNN3_uIcXuOdvfH8w,1287
|
18
|
-
earthscope_sdk/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
19
|
-
earthscope_sdk/config/_compat.py,sha256=P3F5y_Kf5zp9m9uOhl1Bp3ke6expxq4Sm9AeVaBbAHk,4610
|
20
|
-
earthscope_sdk/config/_util.py,sha256=RZ6zvKrvjUkO7i69s7AVoIDhamRg4x71CAZLnucr9QM,1249
|
21
|
-
earthscope_sdk/config/error.py,sha256=jh25q-b317lAvp32WwQw0zdYoV-MxZtg-n5FgZOMymI,95
|
22
|
-
earthscope_sdk/config/models.py,sha256=CarL0O6RjFtufsc-q7g61uBEvETLjQr6HSmjCc0EVig,5775
|
23
|
-
earthscope_sdk/config/settings.py,sha256=I2DwEvfmETcaYbSvUybs0EIih0yiJO9D46WnWzKPqbo,8812
|
24
|
-
earthscope_sdk-1.0.0b0.dist-info/LICENSE,sha256=E_MrVXxRaMQNpvZhsDuz_J9N_ux7dlL_WpYSsE391HU,11349
|
25
|
-
earthscope_sdk-1.0.0b0.dist-info/METADATA,sha256=jbeHzNrmHRZUGOFai1WmCDK1CQ-kWSbIsaNRjHq_WhA,17935
|
26
|
-
earthscope_sdk-1.0.0b0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
27
|
-
earthscope_sdk-1.0.0b0.dist-info/top_level.txt,sha256=zTtIT9yN3JPJF7TqmTzqQcAvZZe4pAm907DLoGa5T_E,15
|
28
|
-
earthscope_sdk-1.0.0b0.dist-info/RECORD,,
|
File without changes
|
File without changes
|